mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-15 18:31:05 +00:00
im-online: use EstimateNextSessionRotation to get better estimates of session progress (#8242)
* frame-support: add method to estimate current session progress * im-online: use EstimateNextSessionRotation trait to delay heartbeats * node: fix im-online pallet instantiation * frame-support: fix docs * frame: fix tests * pallet-session: last block of periodic session means 100% session progress * pallet-session: add test for periodic session progress * pallet-babe: fix epoch progress and add test * frame-support: return weight with session estimates * pallet-im-online: add test for session progress logic
This commit is contained in:
@@ -813,7 +813,6 @@ impl pallet_sudo::Config for Runtime {
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const SessionDuration: BlockNumber = EPOCH_DURATION_IN_SLOTS as _;
|
||||
pub const ImOnlineUnsignedPriority: TransactionPriority = TransactionPriority::max_value();
|
||||
/// We prioritize im-online heartbeats over election solution submission.
|
||||
pub const StakingUnsignedPriority: TransactionPriority = TransactionPriority::max_value() / 2;
|
||||
@@ -880,8 +879,8 @@ impl<C> frame_system::offchain::SendTransactionTypes<C> for Runtime where
|
||||
impl pallet_im_online::Config for Runtime {
|
||||
type AuthorityId = ImOnlineId;
|
||||
type Event = Event;
|
||||
type NextSessionRotation = Babe;
|
||||
type ValidatorSet = Historical;
|
||||
type SessionDuration = SessionDuration;
|
||||
type ReportUnresponsiveness = Offences;
|
||||
type UnsignedPriority = ImOnlineUnsignedPriority;
|
||||
type WeightInfo = pallet_im_online::weights::SubstrateWeight<Runtime>;
|
||||
|
||||
@@ -34,7 +34,7 @@ use sp_application_crypto::Public;
|
||||
use sp_runtime::{
|
||||
generic::DigestItem,
|
||||
traits::{IsMember, One, SaturatedConversion, Saturating, Zero},
|
||||
ConsensusEngineId, KeyTypeId,
|
||||
ConsensusEngineId, KeyTypeId, Percent,
|
||||
};
|
||||
use sp_session::{GetSessionNumber, GetValidatorCount};
|
||||
use sp_std::prelude::*;
|
||||
@@ -780,14 +780,25 @@ impl<T: Config> frame_support::traits::EstimateNextSessionRotation<T::BlockNumbe
|
||||
T::EpochDuration::get().saturated_into()
|
||||
}
|
||||
|
||||
fn estimate_next_session_rotation(now: T::BlockNumber) -> Option<T::BlockNumber> {
|
||||
Self::next_expected_epoch_change(now)
|
||||
fn estimate_current_session_progress(_now: T::BlockNumber) -> (Option<Percent>, Weight) {
|
||||
let elapsed = CurrentSlot::get().saturating_sub(Self::current_epoch_start()) + 1;
|
||||
|
||||
(
|
||||
Some(Percent::from_rational_approximation(
|
||||
*elapsed,
|
||||
T::EpochDuration::get(),
|
||||
)),
|
||||
// Read: Current Slot, Epoch Index, Genesis Slot
|
||||
T::DbWeight::get().reads(3),
|
||||
)
|
||||
}
|
||||
|
||||
// The validity of this weight depends on the implementation of `estimate_next_session_rotation`
|
||||
fn weight(_now: T::BlockNumber) -> Weight {
|
||||
// Read: Current Slot, Epoch Index, Genesis Slot
|
||||
T::DbWeight::get().reads(3)
|
||||
fn estimate_next_session_rotation(now: T::BlockNumber) -> (Option<T::BlockNumber>, Weight) {
|
||||
(
|
||||
Self::next_expected_epoch_change(now),
|
||||
// Read: Current Slot, Epoch Index, Genesis Slot
|
||||
T::DbWeight::get().reads(3),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -260,7 +260,7 @@ impl Config for Test {
|
||||
pub fn go_to_block(n: u64, s: u64) {
|
||||
use frame_support::traits::OnFinalize;
|
||||
|
||||
System::on_finalize(System::block_number());
|
||||
Babe::on_finalize(System::block_number());
|
||||
Session::on_finalize(System::block_number());
|
||||
Staking::on_finalize(System::block_number());
|
||||
|
||||
@@ -274,14 +274,8 @@ pub fn go_to_block(n: u64, s: u64) {
|
||||
let pre_digest = make_secondary_plain_pre_digest(0, s.into());
|
||||
|
||||
System::initialize(&n, &parent_hash, &pre_digest, InitKind::Full);
|
||||
System::set_block_number(n);
|
||||
Timestamp::set_timestamp(n);
|
||||
|
||||
if s > 1 {
|
||||
CurrentSlot::put(Slot::from(s));
|
||||
}
|
||||
|
||||
System::on_initialize(n);
|
||||
Babe::on_initialize(n);
|
||||
Session::on_initialize(n);
|
||||
Staking::on_initialize(n);
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
use super::{Call, *};
|
||||
use frame_support::{
|
||||
assert_err, assert_ok,
|
||||
traits::{Currency, OnFinalize},
|
||||
traits::{Currency, EstimateNextSessionRotation, OnFinalize},
|
||||
weights::{GetDispatchInfo, Pays},
|
||||
};
|
||||
use mock::*;
|
||||
use pallet_session::ShouldEndSession;
|
||||
use sp_consensus_babe::{AllowedSlots, Slot, BabeEpochConfiguration};
|
||||
use sp_consensus_babe::{AllowedSlots, BabeEpochConfiguration, Slot};
|
||||
use sp_core::crypto::Pair;
|
||||
|
||||
const EMPTY_RANDOMNESS: [u8; 32] = [
|
||||
@@ -220,6 +220,38 @@ fn can_predict_next_epoch_change() {
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_estimate_current_epoch_progress() {
|
||||
new_test_ext(1).execute_with(|| {
|
||||
assert_eq!(<Test as Config>::EpochDuration::get(), 3);
|
||||
|
||||
// with BABE the genesis block is not part of any epoch, the first epoch starts at block #1,
|
||||
// therefore its last block should be #3
|
||||
for i in 1u64..4 {
|
||||
progress_to_block(i);
|
||||
|
||||
assert_eq!(Babe::estimate_next_session_rotation(i).0.unwrap(), 4);
|
||||
|
||||
// the last block of the epoch must have 100% progress.
|
||||
if Babe::estimate_next_session_rotation(i).0.unwrap() - 1 == i {
|
||||
assert_eq!(
|
||||
Babe::estimate_current_session_progress(i).0.unwrap(),
|
||||
Percent::from_percent(100)
|
||||
);
|
||||
} else {
|
||||
assert!(Babe::estimate_current_session_progress(i).0.unwrap() < Percent::from_percent(100));
|
||||
}
|
||||
}
|
||||
|
||||
// the first block of the new epoch counts towards the epoch progress as well
|
||||
progress_to_block(4);
|
||||
assert_eq!(
|
||||
Babe::estimate_current_session_progress(4).0.unwrap(),
|
||||
Percent::from_percent(33),
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_enact_next_config() {
|
||||
new_test_ext(1).execute_with(|| {
|
||||
|
||||
@@ -81,20 +81,24 @@ use sp_std::prelude::*;
|
||||
use sp_std::convert::TryInto;
|
||||
use sp_runtime::{
|
||||
offchain::storage::StorageValueRef,
|
||||
RuntimeDebug,
|
||||
traits::{Convert, Member, Saturating, AtLeast32BitUnsigned}, Perbill,
|
||||
traits::{AtLeast32BitUnsigned, Convert, Member, Saturating},
|
||||
transaction_validity::{
|
||||
TransactionValidity, ValidTransaction, InvalidTransaction, TransactionSource,
|
||||
TransactionPriority,
|
||||
InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
|
||||
ValidTransaction,
|
||||
},
|
||||
Perbill, Percent, RuntimeDebug,
|
||||
};
|
||||
use sp_staking::{
|
||||
SessionIndex,
|
||||
offence::{ReportOffence, Offence, Kind},
|
||||
};
|
||||
use frame_support::{
|
||||
decl_module, decl_event, decl_storage, Parameter, decl_error,
|
||||
traits::{Get, ValidatorSet, ValidatorSetWithIdentification, OneSessionHandler},
|
||||
decl_error, decl_event, decl_module, decl_storage,
|
||||
traits::{
|
||||
EstimateNextSessionRotation, Get, OneSessionHandler, ValidatorSet,
|
||||
ValidatorSetWithIdentification,
|
||||
},
|
||||
Parameter,
|
||||
};
|
||||
use frame_system::ensure_none;
|
||||
use frame_system::offchain::{
|
||||
@@ -181,7 +185,7 @@ impl<BlockNumber: PartialEq + AtLeast32BitUnsigned + Copy> HeartbeatStatus<Block
|
||||
/// Error which may occur while executing the off-chain code.
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
enum OffchainErr<BlockNumber> {
|
||||
TooEarly(BlockNumber),
|
||||
TooEarly,
|
||||
WaitingForInclusion(BlockNumber),
|
||||
AlreadyOnline(u32),
|
||||
FailedSigning,
|
||||
@@ -193,8 +197,8 @@ enum OffchainErr<BlockNumber> {
|
||||
impl<BlockNumber: sp_std::fmt::Debug> sp_std::fmt::Debug for OffchainErr<BlockNumber> {
|
||||
fn fmt(&self, fmt: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
|
||||
match *self {
|
||||
OffchainErr::TooEarly(ref block) =>
|
||||
write!(fmt, "Too early to send heartbeat, next expected at {:?}", block),
|
||||
OffchainErr::TooEarly =>
|
||||
write!(fmt, "Too early to send heartbeat."),
|
||||
OffchainErr::WaitingForInclusion(ref block) =>
|
||||
write!(fmt, "Heartbeat already sent at {:?}. Waiting for inclusion.", block),
|
||||
OffchainErr::AlreadyOnline(auth_idx) =>
|
||||
@@ -245,24 +249,24 @@ pub trait Config: SendTransactionTypes<Call<Self>> + frame_system::Config {
|
||||
/// The overarching event type.
|
||||
type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;
|
||||
|
||||
/// An expected duration of the session.
|
||||
///
|
||||
/// This parameter is used to determine the longevity of `heartbeat` transaction
|
||||
/// and a rough time when we should start considering sending heartbeats,
|
||||
/// since the workers avoids sending them at the very beginning of the session, assuming
|
||||
/// there is a chance the authority will produce a block and they won't be necessary.
|
||||
type SessionDuration: Get<Self::BlockNumber>;
|
||||
|
||||
/// A type for retrieving the validators supposed to be online in a session.
|
||||
type ValidatorSet: ValidatorSetWithIdentification<Self::AccountId>;
|
||||
|
||||
/// A trait that allows us to estimate the current session progress and also the
|
||||
/// average session length.
|
||||
///
|
||||
/// This parameter is used to determine the longevity of `heartbeat` transaction and a
|
||||
/// rough time when we should start considering sending heartbeats, since the workers
|
||||
/// avoids sending them at the very beginning of the session, assuming there is a
|
||||
/// chance the authority will produce a block and they won't be necessary.
|
||||
type NextSessionRotation: EstimateNextSessionRotation<Self::BlockNumber>;
|
||||
|
||||
/// A type that gives us the ability to submit unresponsiveness offence reports.
|
||||
type ReportUnresponsiveness:
|
||||
ReportOffence<
|
||||
Self::AccountId,
|
||||
IdentificationTuple<Self>,
|
||||
UnresponsivenessOffence<IdentificationTuple<Self>>,
|
||||
>;
|
||||
type ReportUnresponsiveness: ReportOffence<
|
||||
Self::AccountId,
|
||||
IdentificationTuple<Self>,
|
||||
UnresponsivenessOffence<IdentificationTuple<Self>>,
|
||||
>;
|
||||
|
||||
/// A configuration for base priority of unsigned transactions.
|
||||
///
|
||||
@@ -290,12 +294,17 @@ decl_event!(
|
||||
|
||||
decl_storage! {
|
||||
trait Store for Module<T: Config> as ImOnline {
|
||||
/// The block number after which it's ok to send heartbeats in current session.
|
||||
/// The block number after which it's ok to send heartbeats in the current
|
||||
/// session.
|
||||
///
|
||||
/// At the beginning of each session we set this to a value that should
|
||||
/// fall roughly in the middle of the session duration.
|
||||
/// The idea is to first wait for the validators to produce a block
|
||||
/// in the current session, so that the heartbeat later on will not be necessary.
|
||||
/// At the beginning of each session we set this to a value that should fall
|
||||
/// roughly in the middle of the session duration. The idea is to first wait for
|
||||
/// the validators to produce a block in the current session, so that the
|
||||
/// heartbeat later on will not be necessary.
|
||||
///
|
||||
/// This value will only be used as a fallback if we fail to get a proper session
|
||||
/// progress estimate from `NextSessionRotation`, as those estimates should be
|
||||
/// more accurate then the value we calculate for `HeartbeatAfter`.
|
||||
HeartbeatAfter get(fn heartbeat_after): T::BlockNumber;
|
||||
|
||||
/// The current set of keys that may issue a heartbeat.
|
||||
@@ -469,19 +478,34 @@ impl<T: Config> Module<T> {
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn send_heartbeats(block_number: T::BlockNumber)
|
||||
-> OffchainResult<T, impl Iterator<Item=OffchainResult<T, ()>>>
|
||||
{
|
||||
let heartbeat_after = <HeartbeatAfter<T>>::get();
|
||||
if block_number < heartbeat_after {
|
||||
return Err(OffchainErr::TooEarly(heartbeat_after))
|
||||
pub(crate) fn send_heartbeats(
|
||||
block_number: T::BlockNumber,
|
||||
) -> OffchainResult<T, impl Iterator<Item = OffchainResult<T, ()>>> {
|
||||
const HALF_SESSION: Percent = Percent::from_percent(50);
|
||||
|
||||
let too_early = if let (Some(progress), _) =
|
||||
T::NextSessionRotation::estimate_current_session_progress(block_number)
|
||||
{
|
||||
// we try to get an estimate of the current session progress first since it
|
||||
// should provide more accurate results and send the heartbeat if we're halfway
|
||||
// through the session.
|
||||
progress < HALF_SESSION
|
||||
} else {
|
||||
// otherwise we fallback to using the block number calculated at the beginning
|
||||
// of the session that should roughly correspond to the middle of the session
|
||||
let heartbeat_after = <HeartbeatAfter<T>>::get();
|
||||
block_number < heartbeat_after
|
||||
};
|
||||
|
||||
if too_early {
|
||||
return Err(OffchainErr::TooEarly);
|
||||
}
|
||||
|
||||
let session_index = T::ValidatorSet::session_index();
|
||||
let validators_len = Keys::<T>::decode_len().unwrap_or_default() as u32;
|
||||
|
||||
Ok(Self::local_authority_keys()
|
||||
.map(move |(authority_index, key)|
|
||||
Ok(
|
||||
Self::local_authority_keys().map(move |(authority_index, key)| {
|
||||
Self::send_single_heartbeat(
|
||||
authority_index,
|
||||
key,
|
||||
@@ -489,7 +513,8 @@ impl<T: Config> Module<T> {
|
||||
block_number,
|
||||
validators_len,
|
||||
)
|
||||
))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -648,7 +673,7 @@ impl<T: Config> OneSessionHandler<T::AccountId> for Module<T> {
|
||||
// Since we consider producing blocks as being online,
|
||||
// the heartbeat is deferred a bit to prevent spamming.
|
||||
let block_number = <frame_system::Module<T>>::block_number();
|
||||
let half_session = T::SessionDuration::get() / 2u32.into();
|
||||
let half_session = T::NextSessionRotation::average_session_length() / 2u32.into();
|
||||
<HeartbeatAfter<T>>::put(block_number + half_session);
|
||||
|
||||
// Remember who the authorities are for the new session.
|
||||
@@ -699,10 +724,7 @@ const INVALID_VALIDATORS_LEN: u8 = 10;
|
||||
impl<T: Config> frame_support::unsigned::ValidateUnsigned for Module<T> {
|
||||
type Call = Call<T>;
|
||||
|
||||
fn validate_unsigned(
|
||||
_source: TransactionSource,
|
||||
call: &Self::Call,
|
||||
) -> TransactionValidity {
|
||||
fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
|
||||
if let Call::heartbeat(heartbeat, signature) = call {
|
||||
if <Module<T>>::is_online(heartbeat.authority_index) {
|
||||
// we already received a heartbeat for this authority
|
||||
@@ -737,9 +759,12 @@ impl<T: Config> frame_support::unsigned::ValidateUnsigned for Module<T> {
|
||||
ValidTransaction::with_tag_prefix("ImOnline")
|
||||
.priority(T::UnsignedPriority::get())
|
||||
.and_provides((current_session, authority_id))
|
||||
.longevity(TryInto::<u64>::try_into(
|
||||
T::SessionDuration::get() / 2u32.into()
|
||||
).unwrap_or(64_u64))
|
||||
.longevity(
|
||||
TryInto::<u64>::try_into(
|
||||
T::NextSessionRotation::average_session_length() / 2u32.into(),
|
||||
)
|
||||
.unwrap_or(64_u64),
|
||||
)
|
||||
.propagate(true)
|
||||
.build()
|
||||
} else {
|
||||
|
||||
@@ -21,15 +21,19 @@
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use crate::Config;
|
||||
use sp_runtime::Perbill;
|
||||
use sp_staking::{SessionIndex, offence::{ReportOffence, OffenceError}};
|
||||
use sp_runtime::testing::{Header, UintAuthorityId, TestXt};
|
||||
use sp_runtime::traits::{IdentityLookup, BlakeTwo256, ConvertInto};
|
||||
use sp_core::H256;
|
||||
use frame_support::parameter_types;
|
||||
use crate as imonline;
|
||||
use frame_support::{parameter_types, weights::Weight};
|
||||
use pallet_session::historical as pallet_session_historical;
|
||||
use sp_core::H256;
|
||||
use sp_runtime::testing::{Header, TestXt, UintAuthorityId};
|
||||
use sp_runtime::traits::{BlakeTwo256, ConvertInto, IdentityLookup};
|
||||
use sp_runtime::{Perbill, Percent};
|
||||
use sp_staking::{
|
||||
offence::{OffenceError, ReportOffence},
|
||||
SessionIndex,
|
||||
};
|
||||
|
||||
use crate as imonline;
|
||||
use crate::Config;
|
||||
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Runtime>;
|
||||
type Block = frame_system::mocking::MockBlock<Runtime>;
|
||||
@@ -176,6 +180,41 @@ impl pallet_authorship::Config for Runtime {
|
||||
type EventHandler = ImOnline;
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub static MOCK_CURRENT_SESSION_PROGRESS: RefCell<Option<Option<Percent>>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub static MOCK_AVERAGE_SESSION_LENGTH: RefCell<Option<u64>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
pub struct TestNextSessionRotation;
|
||||
|
||||
impl frame_support::traits::EstimateNextSessionRotation<u64> for TestNextSessionRotation {
|
||||
fn average_session_length() -> u64 {
|
||||
// take the mock result if any and return it
|
||||
let mock = MOCK_AVERAGE_SESSION_LENGTH.with(|p| p.borrow_mut().take());
|
||||
|
||||
mock.unwrap_or(pallet_session::PeriodicSessions::<Period, Offset>::average_session_length())
|
||||
}
|
||||
|
||||
fn estimate_current_session_progress(now: u64) -> (Option<Percent>, Weight) {
|
||||
let (estimate, weight) =
|
||||
pallet_session::PeriodicSessions::<Period, Offset>::estimate_current_session_progress(
|
||||
now,
|
||||
);
|
||||
|
||||
// take the mock result if any and return it
|
||||
let mock = MOCK_CURRENT_SESSION_PROGRESS.with(|p| p.borrow_mut().take());
|
||||
|
||||
(mock.unwrap_or(estimate), weight)
|
||||
}
|
||||
|
||||
fn estimate_next_session_rotation(now: u64) -> (Option<u64>, Weight) {
|
||||
pallet_session::PeriodicSessions::<Period, Offset>::estimate_next_session_rotation(now)
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const UnsignedPriority: u64 = 1 << 20;
|
||||
}
|
||||
@@ -183,9 +222,9 @@ parameter_types! {
|
||||
impl Config for Runtime {
|
||||
type AuthorityId = UintAuthorityId;
|
||||
type Event = Event;
|
||||
type ReportUnresponsiveness = OffenceHandler;
|
||||
type ValidatorSet = Historical;
|
||||
type SessionDuration = Period;
|
||||
type NextSessionRotation = TestNextSessionRotation;
|
||||
type ReportUnresponsiveness = OffenceHandler;
|
||||
type UnsignedPriority = UnsignedPriority;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
@@ -357,3 +357,86 @@ fn should_not_send_a_report_if_already_online() {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_handle_missing_progress_estimates() {
|
||||
use frame_support::traits::OffchainWorker;
|
||||
|
||||
let mut ext = new_test_ext();
|
||||
let (offchain, _state) = TestOffchainExt::new();
|
||||
let (pool, state) = TestTransactionPoolExt::new();
|
||||
ext.register_extension(OffchainDbExt::new(offchain.clone()));
|
||||
ext.register_extension(OffchainWorkerExt::new(offchain));
|
||||
ext.register_extension(TransactionPoolExt::new(pool));
|
||||
|
||||
ext.execute_with(|| {
|
||||
let block = 1;
|
||||
|
||||
System::set_block_number(block);
|
||||
UintAuthorityId::set_all_keys(vec![0, 1, 2]);
|
||||
|
||||
// buffer new validators
|
||||
Session::rotate_session();
|
||||
|
||||
// enact the change and buffer another one
|
||||
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![0, 1, 2]));
|
||||
Session::rotate_session();
|
||||
|
||||
// we will return `None` on the next call to `estimate_current_session_progress`
|
||||
// and the offchain worker should fallback to checking `HeartbeatAfter`
|
||||
MOCK_CURRENT_SESSION_PROGRESS.with(|p| *p.borrow_mut() = Some(None));
|
||||
ImOnline::offchain_worker(block);
|
||||
|
||||
assert_eq!(state.read().transactions.len(), 3);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_handle_non_linear_session_progress() {
|
||||
// NOTE: this is the reason why we started using `EstimateNextSessionRotation` to figure out if
|
||||
// we should send a heartbeat, it's possible that between successive blocks we progress through
|
||||
// the session more than just one block increment (in BABE session length is defined in slots,
|
||||
// not block numbers).
|
||||
|
||||
let mut ext = new_test_ext();
|
||||
let (offchain, _state) = TestOffchainExt::new();
|
||||
let (pool, _) = TestTransactionPoolExt::new();
|
||||
ext.register_extension(OffchainDbExt::new(offchain.clone()));
|
||||
ext.register_extension(OffchainWorkerExt::new(offchain));
|
||||
ext.register_extension(TransactionPoolExt::new(pool));
|
||||
|
||||
ext.execute_with(|| {
|
||||
UintAuthorityId::set_all_keys(vec![0, 1, 2]);
|
||||
|
||||
// buffer new validator
|
||||
Session::rotate_session();
|
||||
|
||||
// mock the session length as being 10 blocks long,
|
||||
// enact the change and buffer another one
|
||||
VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![0, 1, 2]));
|
||||
|
||||
// mock the session length has being 10 which should make us assume the fallback for half
|
||||
// session will be reached by block 5.
|
||||
MOCK_AVERAGE_SESSION_LENGTH.with(|p| *p.borrow_mut() = Some(10));
|
||||
|
||||
Session::rotate_session();
|
||||
|
||||
// if we don't have valid results for the current session progres then
|
||||
// we'll fallback to `HeartbeatAfter` and only heartbeat on block 5.
|
||||
MOCK_CURRENT_SESSION_PROGRESS.with(|p| *p.borrow_mut() = Some(None));
|
||||
assert_eq!(
|
||||
ImOnline::send_heartbeats(2).err(),
|
||||
Some(OffchainErr::TooEarly),
|
||||
);
|
||||
|
||||
MOCK_CURRENT_SESSION_PROGRESS.with(|p| *p.borrow_mut() = Some(None));
|
||||
assert!(ImOnline::send_heartbeats(5).ok().is_some());
|
||||
|
||||
// if we have a valid current session progress then we'll heartbeat as soon
|
||||
// as we're past 50% of the session regardless of the block number
|
||||
MOCK_CURRENT_SESSION_PROGRESS
|
||||
.with(|p| *p.borrow_mut() = Some(Some(Percent::from_percent(51))));
|
||||
|
||||
assert!(ImOnline::send_heartbeats(2).ok().is_some());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ impl pallet_im_online::Config for Test {
|
||||
type AuthorityId = UintAuthorityId;
|
||||
type Event = Event;
|
||||
type ValidatorSet = Historical;
|
||||
type SessionDuration = Period;
|
||||
type NextSessionRotation = pallet_session::PeriodicSessions<Period, Offset>;
|
||||
type ReportUnresponsiveness = Offences;
|
||||
type UnsignedPriority = ();
|
||||
type WeightInfo = ();
|
||||
|
||||
@@ -116,8 +116,10 @@ pub mod weights;
|
||||
|
||||
use sp_std::{prelude::*, marker::PhantomData, ops::{Sub, Rem}};
|
||||
use codec::Decode;
|
||||
use sp_runtime::{KeyTypeId, Perbill, RuntimeAppPublic};
|
||||
use sp_runtime::traits::{Convert, Zero, Member, OpaqueKeys, Saturating};
|
||||
use sp_runtime::{
|
||||
traits::{AtLeast32BitUnsigned, Convert, Member, One, OpaqueKeys, Zero},
|
||||
KeyTypeId, Perbill, Percent, RuntimeAppPublic,
|
||||
};
|
||||
use sp_staking::SessionIndex;
|
||||
use frame_support::{
|
||||
ensure, decl_module, decl_event, decl_storage, decl_error, ConsensusEngineId, Parameter,
|
||||
@@ -142,16 +144,14 @@ pub trait ShouldEndSession<BlockNumber> {
|
||||
/// The first session will have length of `Offset`, and
|
||||
/// the following sessions will have length of `Period`.
|
||||
/// This may prove nonsensical if `Offset` >= `Period`.
|
||||
pub struct PeriodicSessions<
|
||||
Period,
|
||||
Offset,
|
||||
>(PhantomData<(Period, Offset)>);
|
||||
pub struct PeriodicSessions<Period, Offset>(PhantomData<(Period, Offset)>);
|
||||
|
||||
impl<
|
||||
BlockNumber: Rem<Output=BlockNumber> + Sub<Output=BlockNumber> + Zero + PartialOrd,
|
||||
BlockNumber: Rem<Output = BlockNumber> + Sub<Output = BlockNumber> + Zero + PartialOrd,
|
||||
Period: Get<BlockNumber>,
|
||||
Offset: Get<BlockNumber>,
|
||||
> ShouldEndSession<BlockNumber> for PeriodicSessions<Period, Offset> {
|
||||
> ShouldEndSession<BlockNumber> for PeriodicSessions<Period, Offset>
|
||||
{
|
||||
fn should_end_session(now: BlockNumber) -> bool {
|
||||
let offset = Offset::get();
|
||||
now >= offset && ((now - offset) % Period::get()).is_zero()
|
||||
@@ -159,14 +159,47 @@ impl<
|
||||
}
|
||||
|
||||
impl<
|
||||
BlockNumber: Rem<Output=BlockNumber> + Sub<Output=BlockNumber> + Zero + PartialOrd + Saturating + Clone,
|
||||
BlockNumber: AtLeast32BitUnsigned + Clone,
|
||||
Period: Get<BlockNumber>,
|
||||
Offset: Get<BlockNumber>,
|
||||
> EstimateNextSessionRotation<BlockNumber> for PeriodicSessions<Period, Offset> {
|
||||
fn estimate_next_session_rotation(now: BlockNumber) -> Option<BlockNumber> {
|
||||
Offset: Get<BlockNumber>
|
||||
> EstimateNextSessionRotation<BlockNumber> for PeriodicSessions<Period, Offset>
|
||||
{
|
||||
fn average_session_length() -> BlockNumber {
|
||||
Period::get()
|
||||
}
|
||||
|
||||
fn estimate_current_session_progress(now: BlockNumber) -> (Option<Percent>, Weight) {
|
||||
let offset = Offset::get();
|
||||
let period = Period::get();
|
||||
Some(if now > offset {
|
||||
|
||||
// NOTE: we add one since we assume that the current block has already elapsed,
|
||||
// i.e. when evaluating the last block in the session the progress should be 100%
|
||||
// (0% is never returned).
|
||||
let progress = if now >= offset {
|
||||
let current = (now - offset) % period.clone() + One::one();
|
||||
Some(Percent::from_rational_approximation(
|
||||
current.clone(),
|
||||
period.clone(),
|
||||
))
|
||||
} else {
|
||||
Some(Percent::from_rational_approximation(
|
||||
now + One::one(),
|
||||
offset,
|
||||
))
|
||||
};
|
||||
|
||||
// Weight note: `estimate_current_session_progress` has no storage reads and trivial
|
||||
// computational overhead. There should be no risk to the chain having this weight value be
|
||||
// zero for now. However, this value of zero was not properly calculated, and so it would be
|
||||
// reasonable to come back here and properly calculate the weight of this function.
|
||||
(progress, Zero::zero())
|
||||
}
|
||||
|
||||
fn estimate_next_session_rotation(now: BlockNumber) -> (Option<BlockNumber>, Weight) {
|
||||
let offset = Offset::get();
|
||||
let period = Period::get();
|
||||
|
||||
let next_session = if now > offset {
|
||||
let block_after_last_session = (now.clone() - offset) % period.clone();
|
||||
if block_after_last_session > Zero::zero() {
|
||||
now.saturating_add(period.saturating_sub(block_after_last_session))
|
||||
@@ -179,19 +212,13 @@ impl<
|
||||
}
|
||||
} else {
|
||||
offset
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
fn weight(_now: BlockNumber) -> Weight {
|
||||
// Weight note: `estimate_next_session_rotation` has no storage reads and trivial
|
||||
// computational overhead. There should be no risk to the chain having this weight value be
|
||||
// zero for now. However, this value of zero was not properly calculated, and so it would be
|
||||
// reasonable to come back here and properly calculate the weight of this function.
|
||||
0
|
||||
}
|
||||
|
||||
fn average_session_length() -> BlockNumber {
|
||||
Period::get()
|
||||
(Some(next_session), Zero::zero())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -833,17 +860,13 @@ impl<T: Config, Inner: FindAuthor<u32>> FindAuthor<T::ValidatorId>
|
||||
}
|
||||
|
||||
impl<T: Config> EstimateNextNewSession<T::BlockNumber> for Module<T> {
|
||||
/// This session module always calls new_session and next_session at the same time, hence we
|
||||
/// do a simple proxy and pass the function to next rotation.
|
||||
fn estimate_next_new_session(now: T::BlockNumber) -> Option<T::BlockNumber> {
|
||||
T::NextSessionRotation::estimate_next_session_rotation(now)
|
||||
}
|
||||
|
||||
fn average_session_length() -> T::BlockNumber {
|
||||
T::NextSessionRotation::average_session_length()
|
||||
}
|
||||
|
||||
fn weight(now: T::BlockNumber) -> Weight {
|
||||
T::NextSessionRotation::weight(now)
|
||||
/// This session module always calls new_session and next_session at the same time, hence we
|
||||
/// do a simple proxy and pass the function to next rotation.
|
||||
fn estimate_next_new_session(now: T::BlockNumber) -> (Option<T::BlockNumber>, Weight) {
|
||||
T::NextSessionRotation::estimate_next_session_rotation(now)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +253,6 @@ fn session_changed_flag_works() {
|
||||
|
||||
#[test]
|
||||
fn periodic_session_works() {
|
||||
|
||||
frame_support::parameter_types! {
|
||||
const Period: u64 = 10;
|
||||
const Offset: u64 = 3;
|
||||
@@ -261,24 +260,67 @@ fn periodic_session_works() {
|
||||
|
||||
type P = PeriodicSessions<Period, Offset>;
|
||||
|
||||
// make sure that offset phase behaves correctly
|
||||
for i in 0u64..3 {
|
||||
assert!(!P::should_end_session(i));
|
||||
assert_eq!(P::estimate_next_session_rotation(i).unwrap(), 3);
|
||||
assert_eq!(P::estimate_next_session_rotation(i).0.unwrap(), 3);
|
||||
|
||||
// the last block of the session (i.e. the one before session rotation)
|
||||
// should have progress 100%.
|
||||
if P::estimate_next_session_rotation(i).0.unwrap() - 1 == i {
|
||||
assert_eq!(
|
||||
P::estimate_current_session_progress(i).0.unwrap(),
|
||||
Percent::from_percent(100)
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
P::estimate_current_session_progress(i).0.unwrap() < Percent::from_percent(100)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// we end the session at block #3 and we consider this block the first one
|
||||
// from the next session. since we're past the offset phase it represents
|
||||
// 1/10 of progress.
|
||||
assert!(P::should_end_session(3u64));
|
||||
assert_eq!(P::estimate_next_session_rotation(3u64).unwrap(), 3);
|
||||
assert_eq!(P::estimate_next_session_rotation(3u64).0.unwrap(), 3);
|
||||
assert_eq!(
|
||||
P::estimate_current_session_progress(3u64).0.unwrap(),
|
||||
Percent::from_percent(10),
|
||||
);
|
||||
|
||||
for i in (1u64..10).map(|i| 3 + i) {
|
||||
assert!(!P::should_end_session(i));
|
||||
assert_eq!(P::estimate_next_session_rotation(i).unwrap(), 13);
|
||||
assert_eq!(P::estimate_next_session_rotation(i).0.unwrap(), 13);
|
||||
|
||||
// as with the offset phase the last block of the session must have 100%
|
||||
// progress.
|
||||
if P::estimate_next_session_rotation(i).0.unwrap() - 1 == i {
|
||||
assert_eq!(
|
||||
P::estimate_current_session_progress(i).0.unwrap(),
|
||||
Percent::from_percent(100)
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
P::estimate_current_session_progress(i).0.unwrap() < Percent::from_percent(100)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// the new session starts and we proceed in 1/10 increments.
|
||||
assert!(P::should_end_session(13u64));
|
||||
assert_eq!(P::estimate_next_session_rotation(13u64).unwrap(), 23);
|
||||
assert_eq!(P::estimate_next_session_rotation(13u64).0.unwrap(), 23);
|
||||
assert_eq!(
|
||||
P::estimate_current_session_progress(13u64).0.unwrap(),
|
||||
Percent::from_percent(10)
|
||||
);
|
||||
|
||||
assert!(!P::should_end_session(14u64));
|
||||
assert_eq!(P::estimate_next_session_rotation(14u64).unwrap(), 23);
|
||||
assert_eq!(P::estimate_next_session_rotation(14u64).0.unwrap(), 23);
|
||||
assert_eq!(
|
||||
P::estimate_current_session_progress(14u64).0.unwrap(),
|
||||
Percent::from_percent(20)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1365,7 +1365,10 @@ decl_module! {
|
||||
// either current session final based on the plan, or we're forcing.
|
||||
(Self::is_current_session_final() || Self::will_era_be_forced())
|
||||
{
|
||||
if let Some(next_session_change) = T::NextNewSession::estimate_next_new_session(now) {
|
||||
let (maybe_next_session_change, estimate_next_new_session_weight) =
|
||||
T::NextNewSession::estimate_next_new_session(now);
|
||||
|
||||
if let Some(next_session_change) = maybe_next_session_change {
|
||||
if let Some(remaining) = next_session_change.checked_sub(&now) {
|
||||
if remaining <= T::ElectionLookahead::get() && !remaining.is_zero() {
|
||||
// create snapshot.
|
||||
@@ -1387,7 +1390,7 @@ decl_module! {
|
||||
} else {
|
||||
log!(warn, "Estimating next session change failed.");
|
||||
}
|
||||
add_weight(0, 0, T::NextNewSession::weight(now))
|
||||
add_weight(0, 0, estimate_next_new_session_weight)
|
||||
}
|
||||
// For `era_election_status`, `is_current_session_final`, `will_era_be_forced`
|
||||
add_weight(3, 0, 0);
|
||||
@@ -3365,6 +3368,7 @@ impl<T: Config> sp_election_providers::ElectionDataProvider<T::AccountId, T::Blo
|
||||
let session_length = T::NextNewSession::average_session_length();
|
||||
|
||||
let until_this_session_end = T::NextNewSession::estimate_next_new_session(now)
|
||||
.0
|
||||
.unwrap_or_default()
|
||||
.saturating_sub(now);
|
||||
|
||||
|
||||
@@ -24,12 +24,12 @@ use codec::{FullCodec, Codec, Encode, Decode, EncodeLike};
|
||||
use sp_core::u32_trait::Value as U32;
|
||||
use sp_runtime::{
|
||||
traits::{
|
||||
AtLeast32Bit, AtLeast32BitUnsigned, BadOrigin, Block as BlockT, Bounded, Convert,
|
||||
AtLeast32Bit, AtLeast32BitUnsigned, Block as BlockT, BadOrigin, Convert,
|
||||
MaybeSerializeDeserialize, SaturatedConversion, Saturating, StoredMapError,
|
||||
UniqueSaturatedFrom, UniqueSaturatedInto, Zero,
|
||||
},
|
||||
BoundToRuntimeAppPublic, ConsensusEngineId, DispatchError, DispatchResult, RuntimeAppPublic,
|
||||
RuntimeDebug,
|
||||
BoundToRuntimeAppPublic, ConsensusEngineId, DispatchError, DispatchResult, Percent,
|
||||
RuntimeAppPublic, RuntimeDebug,
|
||||
};
|
||||
use sp_staking::SessionIndex;
|
||||
use crate::dispatch::Parameter;
|
||||
@@ -490,42 +490,56 @@ impl<
|
||||
}
|
||||
}
|
||||
|
||||
/// Something that can estimate at which block the next session rotation will happen.
|
||||
/// Something that can estimate at which block the next session rotation will happen (i.e. a new
|
||||
/// session starts).
|
||||
///
|
||||
/// The accuracy of the estimates is dependent on the specific implementation, but in order to get
|
||||
/// the best estimate possible these methods should be called throughout the duration of the session
|
||||
/// (rather than calling once and storing the result).
|
||||
///
|
||||
/// This should be the same logical unit that dictates `ShouldEndSession` to the session module. No
|
||||
/// Assumptions are made about the scheduling of the sessions.
|
||||
/// assumptions are made about the scheduling of the sessions.
|
||||
pub trait EstimateNextSessionRotation<BlockNumber> {
|
||||
/// Return the average length of a session.
|
||||
///
|
||||
/// This may or may not be accurate.
|
||||
fn average_session_length() -> BlockNumber;
|
||||
|
||||
/// Return an estimate of the current session progress.
|
||||
///
|
||||
/// None should be returned if the estimation fails to come to an answer.
|
||||
fn estimate_current_session_progress(now: BlockNumber) -> (Option<Percent>, Weight);
|
||||
|
||||
/// Return the block number at which the next session rotation is estimated to happen.
|
||||
///
|
||||
/// None should be returned if the estimation fails to come to an answer
|
||||
fn estimate_next_session_rotation(now: BlockNumber) -> Option<BlockNumber>;
|
||||
|
||||
/// Return the weight of calling `estimate_next_session_rotation`
|
||||
fn weight(now: BlockNumber) -> Weight;
|
||||
/// None should be returned if the estimation fails to come to an answer.
|
||||
fn estimate_next_session_rotation(now: BlockNumber) -> (Option<BlockNumber>, Weight);
|
||||
}
|
||||
|
||||
impl<BlockNumber: Bounded + Default> EstimateNextSessionRotation<BlockNumber> for () {
|
||||
impl<BlockNumber: Zero> EstimateNextSessionRotation<BlockNumber> for () {
|
||||
fn average_session_length() -> BlockNumber {
|
||||
Default::default()
|
||||
Zero::zero()
|
||||
}
|
||||
|
||||
fn estimate_next_session_rotation(_: BlockNumber) -> Option<BlockNumber> {
|
||||
Default::default()
|
||||
fn estimate_current_session_progress(_: BlockNumber) -> (Option<Percent>, Weight) {
|
||||
(None, Zero::zero())
|
||||
}
|
||||
|
||||
fn weight(_: BlockNumber) -> Weight {
|
||||
0
|
||||
fn estimate_next_session_rotation(_: BlockNumber) -> (Option<BlockNumber>, Weight) {
|
||||
(None, Zero::zero())
|
||||
}
|
||||
}
|
||||
|
||||
/// Something that can estimate at which block the next `new_session` will be triggered.
|
||||
/// Something that can estimate at which block scheduling of the next session will happen (i.e when
|
||||
/// we will try to fetch new validators).
|
||||
///
|
||||
/// This must always be implemented by the session module.
|
||||
/// This only refers to the point when we fetch the next session details and not when we enact them
|
||||
/// (for enactment there's `EstimateNextSessionRotation`). With `pallet-session` this should be
|
||||
/// triggered whenever `SessionManager::new_session` is called.
|
||||
///
|
||||
/// For example, if we are using a staking module this would be the block when the session module
|
||||
/// would ask staking what the next validator set will be, as such this must always be implemented
|
||||
/// by the session module.
|
||||
pub trait EstimateNextNewSession<BlockNumber> {
|
||||
/// Return the average length of a session.
|
||||
///
|
||||
@@ -533,23 +547,18 @@ pub trait EstimateNextNewSession<BlockNumber> {
|
||||
fn average_session_length() -> BlockNumber;
|
||||
|
||||
/// Return the block number at which the next new session is estimated to happen.
|
||||
fn estimate_next_new_session(now: BlockNumber) -> Option<BlockNumber>;
|
||||
|
||||
/// Return the weight of calling `estimate_next_new_session`
|
||||
fn weight(now: BlockNumber) -> Weight;
|
||||
///
|
||||
/// None should be returned if the estimation fails to come to an answer.
|
||||
fn estimate_next_new_session(_: BlockNumber) -> (Option<BlockNumber>, Weight);
|
||||
}
|
||||
|
||||
impl<BlockNumber: Bounded + Default> EstimateNextNewSession<BlockNumber> for () {
|
||||
impl<BlockNumber: Zero> EstimateNextNewSession<BlockNumber> for () {
|
||||
fn average_session_length() -> BlockNumber {
|
||||
Default::default()
|
||||
Zero::zero()
|
||||
}
|
||||
|
||||
fn estimate_next_new_session(_: BlockNumber) -> Option<BlockNumber> {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn weight(_: BlockNumber) -> Weight {
|
||||
0
|
||||
fn estimate_next_new_session(_: BlockNumber) -> (Option<BlockNumber>, Weight) {
|
||||
(None, Zero::zero())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user