#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::{
traits::{DisabledValidators, FindAuthor, Get, OnTimestampSet, OneSessionHandler},
BoundedSlice, BoundedVec, ConsensusEngineId, Parameter,
};
use sp_consensus_aura::{AuthorityIndex, ConsensusLog, Slot, AURA_ENGINE_ID};
use sp_runtime::{
Percent,
generic::DigestItem,
traits::{IsMember, Member, SaturatedConversion, Saturating, Zero},
RuntimeAppPublic,
};
use sp_std::prelude::*;
pub use pallet::*;
const LOG_TARGET: &str = "runtime::aura";
pub struct MinimumPeriodTimesTwo<T>(sp_std::marker::PhantomData<T>);
impl<T: pallet_timestamp::Config> Get<T::Moment> for MinimumPeriodTimesTwo<T> {
fn get() -> T::Moment {
<T as pallet_timestamp::Config>::MinimumPeriod::get().saturating_mul(2u32.into())
}
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[pallet::config]
pub trait Config: pallet_timestamp::Config + frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type AuthorityId: Member
+ Parameter
+ RuntimeAppPublic
+ MaybeSerializeDeserialize
+ MaxEncodedLen;
type MaxAuthorities: Get<u32>;
#[pallet::constant]
type SessionPeriod: Get<u32>;
#[pallet::constant]
type MaxSplits: Get<u32>;
type DisabledValidators: DisabledValidators;
type AllowMultipleBlocksPerSlot: Get<bool>;
#[cfg(feature = "experimental")]
type SlotDuration: Get<<Self as pallet_timestamp::Config>::Moment>;
}
#[pallet::pallet]
pub struct Pallet<T>(sp_std::marker::PhantomData<T>);
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
DecisionRequired { round_at: u32 },
DecisionAccepted { decisions: u32 }
}
#[pallet::error]
pub enum Error<T> {
InvalidAuthorSlotCount,
IncorrectSlotAlloc,
TooManyValidators,
InvalidAuthorIndexes,
UnknownValidators,
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(n: BlockNumberFor<T>) -> Weight {
if let Some(new_slot) = Self::current_slot_from_digests() {
let current_slot = CurrentSlot::<T>::get();
if T::AllowMultipleBlocksPerSlot::get() {
assert!(current_slot <= new_slot, "Slot must not decrease");
} else {
assert!(current_slot < new_slot, "Slot must increase");
}
CurrentSlot::<T>::put(new_slot);
if let Some(n_authorities) = <Authorities<T>>::decode_len() {
let authority_index = *new_slot % n_authorities as u64;
if T::DisabledValidators::is_disabled(authority_index as u32) {
panic!(
"Validator with index {:?} is disabled and should not be attempting to author blocks.",
authority_index,
);
}
}
let decisions = <TotalDecisions<T>>::get();
if !decisions.is_zero() {
let round_at = <RoundAt<T>>::get();
let slots_each_round = <SlotsEachRound<T>>::get();
let authors_each_round = <AuthorsEachRound<T>>::get();
if !((n % T::SessionPeriod::get().into()).is_zero()) {
if round_at < T::MaxSplits::get() {
let current_round_slot = Percent::from_percent(slots_each_round[round_at as usize] as u8) * T::SessionPeriod::get();
if <LastRoundAt<T>>::get().is_zero() {<LastRoundAt<T>>::put(n);}
let current_round_block = <LastRoundAt<T>>::get().saturating_add(current_round_slot.into());
if n == current_round_block {
let range_to_start = if round_at > 0 {(authors_each_round[round_at as usize].checked_add(1).unwrap()) as usize} else {round_at as usize};
let all_validators = <AllValidators<T>>::get();
let range_to_inclusive = if round_at < T::MaxSplits::get().saturating_sub(1) {
authors_each_round[(round_at+1) as usize] as usize
}else{
all_validators.len().saturating_sub(1)
};
let current_authorities = &all_validators[range_to_start..=range_to_inclusive];
let bounded = <BoundedVec<_, T::MaxAuthorities>>::truncate_from(current_authorities.into());
if current_authorities.len() > 1 {Self::change_authorities(bounded);}
<RoundAt<T>>::put(round_at+1);
<LastRoundAt<T>>::put(n);
}
}else{
<RoundAt<T>>::put(0);
}
}
}
T::DbWeight::get().reads_writes(2, 1)
} else {
T::DbWeight::get().reads(1)
}
}
#[cfg(feature = "try-runtime")]
fn try_state(_: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
Self::do_try_state()
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational, Pays::No))]
pub fn pass_decision(
origin: OriginFor<T>,
slots_each_round: Vec<u32>,
authors_each_round: Vec<u32>,
validators_categorized: Vec<T::AuthorityId>
) -> DispatchResultWithPostInfo {
ensure_root(origin)?;
let all_validators_len =
<AllValidators<T>>::decode_len().ok_or("Failed to decode validators length")?;
frame_support::ensure!(
slots_each_round.len() as u32 == T::MaxSplits::get() &&
authors_each_round.len() == slots_each_round.len(),
Error::<T>::InvalidAuthorSlotCount
);
let slot_sum: u32 = slots_each_round.iter().sum();
frame_support::ensure!(slot_sum == 100, Error::<T>::IncorrectSlotAlloc);
let cmp = Self::is_sorted_and_unique(&authors_each_round); let cmp2 = authors_each_round.iter().any(|&i| i >= (all_validators_len.saturating_sub(1)) as u32);
frame_support::ensure!(cmp, Error::<T>::InvalidAuthorIndexes);
frame_support::ensure!(!cmp2, Error::<T>::TooManyValidators);
frame_support::ensure!(
validators_categorized.len() == all_validators_len,
Error::<T>::TooManyValidators
);
let current_validators = <AllValidators<T>>::get().into_inner();
let diff: Vec<T::AuthorityId> = validators_categorized.iter()
.filter(|item| !current_validators.contains(item)).cloned().collect();
frame_support::ensure!(diff.len().is_zero(), Error::<T>::UnknownValidators);
let slots = <BoundedVec<_, T::MaxSplits>>::try_from(slots_each_round)
.expect("slots can not exceed T::MaxSplits");
let authors = <BoundedVec<_, T::MaxSplits>>::try_from(authors_each_round)
.expect("authors can not exceed T::MaxSplits");
let validators = <BoundedVec<_, T::MaxAuthorities>>::truncate_from(validators_categorized);
Self::change_decision(slots, authors, validators);
Ok(().into())
}
}
#[pallet::storage]
#[pallet::getter(fn total_decision)]
pub(super) type TotalDecisions<T: Config> =
StorageValue<_, u32, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn last_round_at)]
pub(super) type LastRoundAt<T: Config> =
StorageValue<_, BlockNumberFor<T>, ValueQuery>;
#[pallet::storage]
pub(super) type RoundAt<T: Config> =
StorageValue<_, u32, ValueQuery>;
#[pallet::storage]
pub(super) type SlotsEachRound<T: Config> =
StorageValue<_, BoundedVec<u32, T::MaxSplits>, ValueQuery>;
#[pallet::storage]
pub(super) type AuthorsEachRound<T: Config> =
StorageValue<_, BoundedVec<u32, T::MaxSplits>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn all_validators)]
pub(super) type AllValidators<T: Config> =
StorageValue<_, BoundedVec<T::AuthorityId, T::MaxAuthorities>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn authorities)]
pub(super) type Authorities<T: Config> =
StorageValue<_, BoundedVec<T::AuthorityId, T::MaxAuthorities>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn current_slot)]
pub(super) type CurrentSlot<T: Config> = StorageValue<_, Slot, ValueQuery>;
#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
pub authorities: Vec<T::AuthorityId>,
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
Pallet::<T>::initialize_authorities(&self.authorities);
}
}
}
impl<T: Config> Pallet<T> {
pub fn change_authorities(new: BoundedVec<T::AuthorityId, T::MaxAuthorities>) {
if new.is_empty() {
log::warn!(target: LOG_TARGET, "Ignoring empty authority change.");
return
}
<Authorities<T>>::put(&new);
let log = DigestItem::Consensus(
AURA_ENGINE_ID,
ConsensusLog::AuthoritiesChange(new.into_inner()).encode(),
);
<frame_system::Pallet<T>>::deposit_log(log);
}
pub fn change_validators(new: BoundedVec<T::AuthorityId, T::MaxAuthorities>) {
if new.is_empty() {
log::warn!(target: LOG_TARGET, "Ignoring empty validator change.");
return
}
<AllValidators<T>>::put(&new);
Self::deposit_event(Event::DecisionRequired{round_at: <RoundAt<T>>::get()});
}
pub fn change_decision(
slots: BoundedVec<u32, T::MaxSplits>,
authors: BoundedVec<u32, T::MaxSplits>,
validators: BoundedVec<T::AuthorityId, T::MaxAuthorities>
) {
<SlotsEachRound<T>>::put(slots);
<AuthorsEachRound<T>>::put(authors);
<AllValidators<T>>::put(validators);
let decisions = <TotalDecisions<T>>::get();
<TotalDecisions<T>>::put(decisions.checked_add(1).unwrap());
Self::deposit_event(Event::DecisionAccepted{decisions: <TotalDecisions<T>>::get()});
}
pub fn initialize_authorities(authorities: &[T::AuthorityId]) {
if !authorities.is_empty() {
assert!(<Authorities<T>>::get().is_empty(), "Authorities are already initialized!");
let bounded = <BoundedSlice<'_, _, T::MaxAuthorities>>::try_from(authorities)
.expect("Initial authority set must be less than T::MaxAuthorities");
<Authorities<T>>::put(bounded);
<AllValidators<T>>::put(bounded);
<RoundAt<T>>::put(0);
}
}
fn current_slot_from_digests() -> Option<Slot> {
let digest = frame_system::Pallet::<T>::digest();
let pre_runtime_digests = digest.logs.iter().filter_map(|d| d.as_pre_runtime());
for (id, mut data) in pre_runtime_digests {
if id == AURA_ENGINE_ID {
return Slot::decode(&mut data).ok()
}
}
None
}
pub fn slot_duration() -> T::Moment {
#[cfg(feature = "experimental")]
{
T::SlotDuration::get()
}
#[cfg(not(feature = "experimental"))]
{
<T as pallet_timestamp::Config>::MinimumPeriod::get().saturating_mul(2u32.into())
}
}
#[cfg(any(test, feature = "try-runtime"))]
pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
#[allow(clippy::redundant_closure)]
let current_slot =
Self::current_slot_from_digests().unwrap_or_else(|| CurrentSlot::<T>::get());
if !T::AllowMultipleBlocksPerSlot::get() {
frame_support::ensure!(
current_slot < u64::MAX,
"Current slot has reached maximum value and cannot be incremented further.",
);
}
let authorities_len =
<Authorities<T>>::decode_len().ok_or("Failed to decode authorities length")?;
frame_support::ensure!(!authorities_len.is_zero(), "Authorities must be non-empty.");
let authority_index = *current_slot % authorities_len as u64;
frame_support::ensure!(
!T::DisabledValidators::is_disabled(authority_index as u32),
"Current validator is disabled and should not be attempting to author blocks.",
);
Ok(())
}
fn is_sorted_and_unique(authors: &[u32]) -> bool {
authors.windows(2).all(|w| w[0] < w[1])
}
}
impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> {
type Public = T::AuthorityId;
}
impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
type Key = T::AuthorityId;
fn on_genesis_session<'a, I: 'a>(validators: I)
where
I: Iterator<Item = (&'a T::AccountId, T::AuthorityId)>,
{
let authorities = validators.map(|(_, k)| k).collect::<Vec<_>>();
Self::initialize_authorities(&authorities);
}
fn on_new_session<'a, I: 'a>(changed: bool, validators: I, _queued_validators: I)
where
I: Iterator<Item = (&'a T::AccountId, T::AuthorityId)>,
{
if changed {
let next_validators = validators.map(|(_, k)| k).collect::<Vec<_>>();
let last_validators = Self::all_validators();
if last_validators != next_validators {
if next_validators.len() as u32 > T::MaxAuthorities::get() {
log::warn!(
target: LOG_TARGET,
"next authorities list larger than {}, truncating",
T::MaxAuthorities::get(),
);
}
let bounded = <BoundedVec<_, T::MaxAuthorities>>::truncate_from(next_validators);
Self::change_validators(bounded);
}
}
}
fn on_disabled(i: u32) {
let log = DigestItem::Consensus(
AURA_ENGINE_ID,
ConsensusLog::<T::AuthorityId>::OnDisabled(i as AuthorityIndex).encode(),
);
<frame_system::Pallet<T>>::deposit_log(log);
}
}
impl<T: Config> FindAuthor<u32> for Pallet<T> {
fn find_author<'a, I>(digests: I) -> Option<u32>
where
I: 'a + IntoIterator<Item = (ConsensusEngineId, &'a [u8])>,
{
for (id, mut data) in digests.into_iter() {
if id == AURA_ENGINE_ID {
let slot = Slot::decode(&mut data).ok()?;
let author_index = *slot % Authorities::<T>::decode_len().unwrap() as u64;
return Some(author_index as u32)
}
}
None
}
}
#[doc(hidden)]
pub struct FindAccountFromAuthorIndex<T, Inner>(sp_std::marker::PhantomData<(T, Inner)>);
impl<T: Config, Inner: FindAuthor<u32>> FindAuthor<T::AuthorityId>
for FindAccountFromAuthorIndex<T, Inner>
{
fn find_author<'a, I>(digests: I) -> Option<T::AuthorityId>
where
I: 'a + IntoIterator<Item = (ConsensusEngineId, &'a [u8])>,
{
let i = Inner::find_author(digests)?;
let validators = <Pallet<T>>::authorities();
validators.get(i as usize).cloned()
}
}
pub type AuraAuthorId<T> = FindAccountFromAuthorIndex<T, Pallet<T>>;
impl<T: Config> IsMember<T::AuthorityId> for Pallet<T> {
fn is_member(authority_id: &T::AuthorityId) -> bool {
Self::authorities().iter().any(|id| id == authority_id)
}
}
impl<T: Config> OnTimestampSet<T::Moment> for Pallet<T> {
fn on_timestamp_set(moment: T::Moment) {
let slot_duration = Self::slot_duration();
assert!(!slot_duration.is_zero(), "Aura slot duration cannot be zero.");
let timestamp_slot = moment / slot_duration;
let timestamp_slot = Slot::from(timestamp_slot.saturated_into::<u64>());
assert!(
CurrentSlot::<T>::get() == timestamp_slot,
"Timestamp slot must match `CurrentSlot`"
);
}
}