slots: incrementally backoff claiming slots if finality lags behind (#7186)

* babe: backoff authoring blocks when finality lags

* babe: move backoff authoring params to default constructor

* babe: deduplicate the test a bit

* babe: set backoff constants in service

* babe: use better names for backoff authoring block parameters

* babe: remove last unwrap

* babe: slight style tweak

* babe: fix comment

* slots: move backoff block authorship logic to SimpleSlotWorker

* aura: append SlotInfo in on_slot

* slots: use the correct types for parameters

* slots: fix review comments

* aura: add missing backoff authoring blocks parameters

* slots: add comments for default values

* slots: add additional checks in test

* slots: update implementation for new master

* slots: revert the change to SlotInfo

* Fix review comments

* slots: rework unit tests for backing off claiming slots

* slots: add test for asymptotic behaviour for slot claims

* slots: address review comments

* slots: add test for max_interval

* slots: add assertion for intervals between between claimed slots

* slots: remove rustfmt directive

* slots: another attempt at explaining authoring_rate

* slots: up unfinalized_slack to 50 by default

* slots: add tests for time to reach max_interval

* slots: fix typo in comments

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* slots: additional tweaks to comments and info calls

* slots: rename to BackoffAuthoringOnFinalizedHeadLagging

* slots: make the backing off strategy generic

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* slots: implement backoff trait for () for simplicity

* slots: move logging inside backing off function to make it more specific

* aura: add missing function parameter

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
This commit is contained in:
Jon Häggblad
2020-11-11 14:29:19 +01:00
committed by GitHub
parent 48a84f90b5
commit 7cf78c166e
9 changed files with 541 additions and 21 deletions
+2
View File
@@ -3827,6 +3827,7 @@ dependencies = [
"sc-consensus",
"sc-consensus-babe",
"sc-consensus-epochs",
"sc-consensus-slots",
"sc-finality-grandpa",
"sc-keystore",
"sc-network",
@@ -6881,6 +6882,7 @@ dependencies = [
"sc-telemetry",
"sp-api",
"sp-application-crypto",
"sp-arithmetic",
"sp-blockchain",
"sp-consensus",
"sp-consensus-slots",
@@ -111,6 +111,7 @@ pub fn new_full(config: Configuration) -> Result<TaskManager, ServiceError> {
let role = config.role.clone();
let force_authoring = config.force_authoring;
let backoff_authoring_blocks: Option<()> = None;
let name = config.network.node_name.clone();
let enable_grandpa = !config.disable_grandpa;
let prometheus_registry = config.prometheus_registry().cloned();
@@ -155,7 +156,7 @@ pub fn new_full(config: Configuration) -> Result<TaskManager, ServiceError> {
let can_author_with =
sp_consensus::CanAuthorWithNativeVersion::new(client.executor().clone());
let aura = sc_consensus_aura::start_aura::<_, _, _, _, _, AuraPair, _, _, _>(
let aura = sc_consensus_aura::start_aura::<_, _, _, _, _, AuraPair, _, _, _,_>(
sc_consensus_aura::slot_duration(&*client)?,
client.clone(),
select_chain,
@@ -164,6 +165,7 @@ pub fn new_full(config: Configuration) -> Result<TaskManager, ServiceError> {
network.clone(),
inherent_data_providers.clone(),
force_authoring,
backoff_authoring_blocks,
keystore_container.sync_keystore(),
can_author_with,
)?;
+1
View File
@@ -64,6 +64,7 @@ sc-chain-spec = { version = "2.0.0", path = "../../../client/chain-spec" }
sc-consensus = { version = "0.8.0", path = "../../../client/consensus/common" }
sc-transaction-pool = { version = "2.0.0", path = "../../../client/transaction-pool" }
sc-network = { version = "0.8.0", path = "../../../client/network" }
sc-consensus-slots = { version = "0.8.0", path = "../../../client/consensus/slots" }
sc-consensus-babe = { version = "0.8.0", path = "../../../client/consensus/babe" }
grandpa = { version = "0.8.0", package = "sc-finality-grandpa", path = "../../../client/finality-grandpa" }
sc-client-db = { version = "0.8.0", default-features = false, path = "../../../client/db" }
+3
View File
@@ -204,6 +204,8 @@ pub fn new_full_base(
let role = config.role.clone();
let force_authoring = config.force_authoring;
let backoff_authoring_blocks =
Some(sc_consensus_slots::BackoffAuthoringOnFinalizedHeadLagging::default());
let name = config.network.node_name.clone();
let enable_grandpa = !config.disable_grandpa;
let prometheus_registry = config.prometheus_registry().cloned();
@@ -249,6 +251,7 @@ pub fn new_full_base(
sync_oracle: network.clone(),
inherent_data_providers: inherent_data_providers.clone(),
force_authoring,
backoff_authoring_blocks,
babe_link,
can_author_with,
};
+32 -8
View File
@@ -59,7 +59,7 @@ use sp_core::crypto::Public;
use sp_application_crypto::{AppKey, AppPublic};
use sp_runtime::{
generic::{BlockId, OpaqueDigestItemId},
Justification,
traits::NumberFor, Justification,
};
use sp_runtime::traits::{Block as BlockT, Header, DigestItemFor, Zero, Member};
use sp_api::ProvideRuntimeApi;
@@ -73,6 +73,7 @@ use sc_telemetry::{telemetry, CONSENSUS_TRACE, CONSENSUS_DEBUG, CONSENSUS_INFO};
use sc_consensus_slots::{
CheckedHeader, SlotInfo, SlotCompatible, StorageChanges, check_equivocation,
BackoffAuthoringBlocksStrategy,
};
use sp_api::ApiExt;
@@ -138,7 +139,7 @@ impl SlotCompatible for AuraSlotCompatible {
}
/// Start the aura worker. The returned future should be run in a futures executor.
pub fn start_aura<B, C, SC, E, I, P, SO, CAW, Error>(
pub fn start_aura<B, C, SC, E, I, P, SO, CAW, BS, Error>(
slot_duration: SlotDuration,
client: Arc<C>,
select_chain: SC,
@@ -147,11 +148,12 @@ pub fn start_aura<B, C, SC, E, I, P, SO, CAW, Error>(
sync_oracle: SO,
inherent_data_providers: InherentDataProviders,
force_authoring: bool,
backoff_authoring_blocks: Option<BS>,
keystore: SyncCryptoStorePtr,
can_author_with: CAW,
) -> Result<impl Future<Output = ()>, sp_consensus::Error> where
B: BlockT,
C: ProvideRuntimeApi<B> + BlockOf + ProvideCache<B> + AuxStore + Send + Sync,
C: ProvideRuntimeApi<B> + BlockOf + ProvideCache<B> + AuxStore + HeaderBackend<B> + Send + Sync,
C::Api: AuraApi<B, AuthorityId<P>>,
SC: SelectChain<B>,
E: Environment<B, Error = Error> + Send + Sync + 'static,
@@ -163,6 +165,7 @@ pub fn start_aura<B, C, SC, E, I, P, SO, CAW, Error>(
Error: std::error::Error + Send + From<sp_consensus::Error> + 'static,
SO: SyncOracle + Send + Sync + Clone,
CAW: CanAuthorWith<B> + Send,
BS: BackoffAuthoringBlocksStrategy<NumberFor<B>> + Send + 'static,
{
let worker = AuraWorker {
client,
@@ -171,6 +174,7 @@ pub fn start_aura<B, C, SC, E, I, P, SO, CAW, Error>(
keystore,
sync_oracle: sync_oracle.clone(),
force_authoring,
backoff_authoring_blocks,
_key_type: PhantomData::<P>,
};
register_aura_inherent_data_provider(
@@ -188,20 +192,22 @@ pub fn start_aura<B, C, SC, E, I, P, SO, CAW, Error>(
))
}
struct AuraWorker<C, E, I, P, SO> {
struct AuraWorker<C, E, I, P, SO, BS> {
client: Arc<C>,
block_import: Arc<Mutex<I>>,
env: E,
keystore: SyncCryptoStorePtr,
sync_oracle: SO,
force_authoring: bool,
backoff_authoring_blocks: Option<BS>,
_key_type: PhantomData<P>,
}
impl<B, C, E, I, P, Error, SO> sc_consensus_slots::SimpleSlotWorker<B> for AuraWorker<C, E, I, P, SO>
impl<B, C, E, I, P, Error, SO, BS> sc_consensus_slots::SimpleSlotWorker<B>
for AuraWorker<C, E, I, P, SO, BS>
where
B: BlockT,
C: ProvideRuntimeApi<B> + BlockOf + ProvideCache<B> + Sync,
C: ProvideRuntimeApi<B> + BlockOf + ProvideCache<B> + HeaderBackend<B> + Sync,
C::Api: AuraApi<B, AuthorityId<P>>,
E: Environment<B, Error = Error>,
E::Proposer: Proposer<B, Error = Error, Transaction = sp_api::TransactionFor<C, B>>,
@@ -210,6 +216,7 @@ where
P::Public: AppPublic + Public + Member + Encode + Decode + Hash,
P::Signature: TryFrom<Vec<u8>> + Member + Encode + Decode + Hash + Debug,
SO: SyncOracle + Send + Clone,
BS: BackoffAuthoringBlocksStrategy<NumberFor<B>> + Send + 'static,
Error: std::error::Error + Send + From<sp_consensus::Error> + 'static,
{
type BlockImport = I;
@@ -316,6 +323,21 @@ where
self.force_authoring
}
fn should_backoff(&self, slot_number: u64, chain_head: &B::Header) -> bool {
if let Some(ref strategy) = self.backoff_authoring_blocks {
if let Ok(chain_head_slot) = find_pre_digest::<B, P>(chain_head) {
return strategy.should_backoff(
*chain_head.number(),
chain_head_slot,
self.client.info().finalized_number,
slot_number,
self.logging_target(),
);
}
}
false
}
fn sync_oracle(&mut self) -> &mut Self::SyncOracle {
&mut self.sync_oracle
}
@@ -863,7 +885,7 @@ mod tests {
use sp_keyring::sr25519::Keyring;
use sc_client_api::BlockchainEvents;
use sp_consensus_aura::sr25519::AuthorityPair;
use sc_consensus_slots::SimpleSlotWorker;
use sc_consensus_slots::{SimpleSlotWorker, BackoffAuthoringOnFinalizedHeadLagging};
use std::task::Poll;
use sc_block_builder::BlockBuilderProvider;
use sp_runtime::traits::Header as _;
@@ -1012,7 +1034,7 @@ mod tests {
&inherent_data_providers, slot_duration.get()
).expect("Registers aura inherent data provider");
aura_futures.push(start_aura::<_, _, _, _, _, AuthorityPair, _, _, _>(
aura_futures.push(start_aura::<_, _, _, _, _, AuthorityPair, _, _, _, _>(
slot_duration,
client.clone(),
select_chain,
@@ -1021,6 +1043,7 @@ mod tests {
DummyOracle,
inherent_data_providers,
false,
Some(BackoffAuthoringOnFinalizedHeadLagging::default()),
keystore,
sp_consensus::AlwaysCanAuthor,
).expect("Starts aura"));
@@ -1081,6 +1104,7 @@ mod tests {
keystore: keystore.into(),
sync_oracle: DummyOracle.clone(),
force_authoring: false,
backoff_authoring_blocks: Some(BackoffAuthoringOnFinalizedHeadLagging::default()),
_key_type: PhantomData::<AuthorityPair>,
};
+33 -5
View File
@@ -114,6 +114,7 @@ use log::{debug, info, log, trace, warn};
use prometheus_endpoint::Registry;
use sc_consensus_slots::{
SlotInfo, SlotCompatible, StorageChanges, CheckedHeader, check_equivocation,
BackoffAuthoringBlocksStrategy,
};
use sc_consensus_epochs::{
descendent_query, SharedEpochChanges, EpochChangesFor, Epoch as EpochT, ViableEpochDescriptor,
@@ -354,7 +355,7 @@ impl std::ops::Deref for Config {
}
/// Parameters for BABE.
pub struct BabeParams<B: BlockT, C, E, I, SO, SC, CAW> {
pub struct BabeParams<B: BlockT, C, E, I, SO, SC, CAW, BS> {
/// The keystore that manages the keys of the node.
pub keystore: SyncCryptoStorePtr,
@@ -381,6 +382,9 @@ pub struct BabeParams<B: BlockT, C, E, I, SO, SC, CAW> {
/// Force authoring of blocks even if we are offline
pub force_authoring: bool,
/// Strategy and parameters for backing off block production.
pub backoff_authoring_blocks: Option<BS>,
/// The source of timestamps for relative slots
pub babe_link: BabeLink<B>,
@@ -389,7 +393,7 @@ pub struct BabeParams<B: BlockT, C, E, I, SO, SC, CAW> {
}
/// Start the babe worker.
pub fn start_babe<B, C, SC, E, I, SO, CAW, Error>(BabeParams {
pub fn start_babe<B, C, SC, E, I, SO, CAW, BS, Error>(BabeParams {
keystore,
client,
select_chain,
@@ -398,9 +402,10 @@ pub fn start_babe<B, C, SC, E, I, SO, CAW, Error>(BabeParams {
sync_oracle,
inherent_data_providers,
force_authoring,
backoff_authoring_blocks,
babe_link,
can_author_with,
}: BabeParams<B, C, E, I, SO, SC, CAW>) -> Result<
}: BabeParams<B, C, E, I, SO, SC, CAW, BS>) -> Result<
BabeWorker<B>,
sp_consensus::Error,
> where
@@ -416,6 +421,7 @@ pub fn start_babe<B, C, SC, E, I, SO, CAW, Error>(BabeParams {
Error: std::error::Error + Send + From<ConsensusError> + From<I::Error> + 'static,
SO: SyncOracle + Send + Sync + Clone + 'static,
CAW: CanAuthorWith<B> + Send + 'static,
BS: BackoffAuthoringBlocksStrategy<NumberFor<B>> + Send + 'static,
{
let config = babe_link.config;
let slot_notification_sinks = Arc::new(Mutex::new(Vec::new()));
@@ -426,6 +432,7 @@ pub fn start_babe<B, C, SC, E, I, SO, CAW, Error>(BabeParams {
env,
sync_oracle: sync_oracle.clone(),
force_authoring,
backoff_authoring_blocks,
keystore,
epoch_changes: babe_link.epoch_changes.clone(),
slot_notification_sinks: slot_notification_sinks.clone(),
@@ -490,19 +497,22 @@ impl<B: BlockT> futures::Future for BabeWorker<B> {
/// Slot notification sinks.
type SlotNotificationSinks<B> = Arc<Mutex<Vec<Sender<(u64, ViableEpochDescriptor<<B as BlockT>::Hash, NumberFor<B>, Epoch>)>>>>;
struct BabeSlotWorker<B: BlockT, C, E, I, SO> {
struct BabeSlotWorker<B: BlockT, C, E, I, SO, BS> {
client: Arc<C>,
block_import: Arc<Mutex<I>>,
env: E,
sync_oracle: SO,
force_authoring: bool,
backoff_authoring_blocks: Option<BS>,
keystore: SyncCryptoStorePtr,
epoch_changes: SharedEpochChanges<B, Epoch>,
slot_notification_sinks: SlotNotificationSinks<B>,
config: Config,
}
impl<B, C, E, I, Error, SO> sc_consensus_slots::SimpleSlotWorker<B> for BabeSlotWorker<B, C, E, I, SO> where
impl<B, C, E, I, Error, SO, BS> sc_consensus_slots::SimpleSlotWorker<B>
for BabeSlotWorker<B, C, E, I, SO, BS>
where
B: BlockT,
C: ProvideRuntimeApi<B> +
ProvideCache<B> +
@@ -513,6 +523,7 @@ impl<B, C, E, I, Error, SO> sc_consensus_slots::SimpleSlotWorker<B> for BabeSlot
E::Proposer: Proposer<B, Error = Error, Transaction = sp_api::TransactionFor<C, B>>,
I: BlockImport<B, Transaction = sp_api::TransactionFor<C, B>> + Send + Sync + 'static,
SO: SyncOracle + Send + Clone,
BS: BackoffAuthoringBlocksStrategy<NumberFor<B>>,
Error: std::error::Error + Send + From<ConsensusError> + From<I::Error> + 'static,
{
type EpochData = ViableEpochDescriptor<B::Hash, NumberFor<B>, Epoch>;
@@ -657,6 +668,23 @@ impl<B, C, E, I, Error, SO> sc_consensus_slots::SimpleSlotWorker<B> for BabeSlot
self.force_authoring
}
fn should_backoff(&self, slot_number: u64, chain_head: &B::Header) -> bool {
if let Some(ref strategy) = self.backoff_authoring_blocks {
if let Ok(chain_head_slot) = find_pre_digest::<B>(chain_head)
.map(|digest| digest.slot_number())
{
return strategy.should_backoff(
*chain_head.number(),
chain_head_slot,
self.client.info().finalized_number,
slot_number,
self.logging_target(),
);
}
}
false
}
fn sync_oracle(&mut self) -> &mut Self::SyncOracle {
&mut self.sync_oracle
}
@@ -33,6 +33,7 @@ use sp_consensus_babe::{
make_transcript,
make_transcript_data,
};
use sc_consensus_slots::BackoffAuthoringOnFinalizedHeadLagging;
use sc_block_builder::{BlockBuilder, BlockBuilderProvider};
use sp_consensus::{
NoNetwork as DummyOracle, Proposal, RecordProof, AlwaysCanAuthor,
@@ -434,6 +435,7 @@ fn run_one_test(
sync_oracle: DummyOracle,
inherent_data_providers: data.inherent_data_providers.clone(),
force_authoring: false,
backoff_authoring_blocks: Some(BackoffAuthoringOnFinalizedHeadLagging::default()),
babe_link: data.link.clone(),
keystore,
can_author_with: sp_consensus::AlwaysCanAuthor,
@@ -19,6 +19,7 @@ sc-client-api = { version = "2.0.0", path = "../../api" }
sp-core = { version = "2.0.0", path = "../../../primitives/core" }
sp-trie = { version = "2.0.0", path = "../../../primitives/trie" }
sp-application-crypto = { version = "2.0.0", path = "../../../primitives/application-crypto" }
sp-arithmetic = { version = "2.0.0", path = "../../../primitives/arithmetic" }
sp-blockchain = { version = "2.0.0", path = "../../../primitives/blockchain" }
sp-consensus-slots = { version = "0.8.0", path = "../../../primitives/consensus/slots" }
sp-runtime = { version = "2.0.0", path = "../../../primitives/runtime" }
+464 -7
View File
@@ -29,18 +29,21 @@ pub use slots::SlotInfo;
use slots::Slots;
pub use aux_schema::{check_equivocation, MAX_SLOT_CAPACITY, PRUNING_BOUND};
use std::{fmt::Debug, ops::Deref, pin::Pin, sync::Arc, time::{Instant, Duration}};
use codec::{Decode, Encode};
use sp_consensus::{BlockImport, Proposer, SyncOracle, SelectChain, CanAuthorWith, SlotData, RecordProof};
use futures::{prelude::*, future::{self, Either}};
use futures_timer::Delay;
use sp_inherents::{InherentData, InherentDataProviders};
use log::{debug, error, info, warn};
use sp_runtime::generic::BlockId;
use sp_runtime::traits::{Block as BlockT, Header, HashFor, NumberFor};
use sp_api::{ProvideRuntimeApi, ApiRef};
use std::{fmt::Debug, ops::Deref, pin::Pin, sync::Arc, time::{Instant, Duration}};
use sc_telemetry::{telemetry, CONSENSUS_DEBUG, CONSENSUS_WARN, CONSENSUS_INFO};
use parking_lot::Mutex;
use sp_api::{ProvideRuntimeApi, ApiRef};
use sp_arithmetic::traits::BaseArithmetic;
use sp_consensus::{BlockImport, Proposer, SyncOracle, SelectChain, CanAuthorWith, SlotData, RecordProof};
use sp_inherents::{InherentData, InherentDataProviders};
use sp_runtime::{
generic::BlockId,
traits::{Block as BlockT, Header, HashFor, NumberFor}
};
use sc_telemetry::{telemetry, CONSENSUS_DEBUG, CONSENSUS_WARN, CONSENSUS_INFO};
/// The changes that need to applied to the storage to create the state for a block.
///
@@ -158,6 +161,16 @@ pub trait SimpleSlotWorker<B: BlockT> {
/// Whether to force authoring if offline.
fn force_authoring(&self) -> bool;
/// Returns whether the block production should back off.
///
/// By default this function always returns `false`.
///
/// An example strategy that back offs if the finalized head is lagging too much behind the tip
/// is implemented by [`BackoffAuthoringOnFinalizedHeadLagging`].
fn should_backoff(&self, _slot_number: u64, _chain_head: &B::Header) -> bool {
false
}
/// Returns a handle to a `SyncOracle`.
fn sync_oracle(&mut self) -> &mut Self::SyncOracle;
@@ -249,6 +262,10 @@ pub trait SimpleSlotWorker<B: BlockT> {
Some(claim) => claim,
};
if self.should_backoff(slot_number, &chain_head) {
return Box::pin(future::ready(None));
}
debug!(
target: self.logging_target(),
"Starting authorship at slot {}; timestamp = {}",
@@ -583,9 +600,110 @@ pub fn slot_lenience_linear(parent_slot: u64, slot_info: &SlotInfo) -> Option<Du
}
}
/// Trait for providing the strategy for when to backoff block authoring.
pub trait BackoffAuthoringBlocksStrategy<N> {
/// Returns true if we should backoff authoring new blocks.
fn should_backoff(
&self,
chain_head_number: N,
chain_head_slot: u64,
finalized_number: N,
slow_now: u64,
logging_target: &str,
) -> bool;
}
/// A simple default strategy for how to decide backing off authoring blocks if the number of
/// unfinalized blocks grows too large.
#[derive(Clone)]
pub struct BackoffAuthoringOnFinalizedHeadLagging<N> {
/// The max interval to backoff when authoring blocks, regardless of delay in finality.
pub max_interval: N,
/// The number of unfinalized blocks allowed before starting to consider to backoff authoring
/// blocks. Note that depending on the value for `authoring_bias`, there might still be an
/// additional wait until block authorship starts getting declined.
pub unfinalized_slack: N,
/// Scales the backoff rate. A higher value effectively means we backoff slower, taking longer
/// time to reach the maximum backoff as the unfinalized head of chain grows.
pub authoring_bias: N,
}
/// These parameters is supposed to be some form of sensible defaults.
impl<N: BaseArithmetic> Default for BackoffAuthoringOnFinalizedHeadLagging<N> {
fn default() -> Self {
Self {
// Never wait more than 100 slots before authoring blocks, regardless of delay in
// finality.
max_interval: 100.into(),
// Start to consider backing off block authorship once we have 50 or more unfinalized
// blocks at the head of the chain.
unfinalized_slack: 50.into(),
// A reasonable default for the authoring bias, or reciprocal interval scaling, is 2.
// Effectively meaning that consider the unfinalized head suffix length to grow half as
// fast as in actuality.
authoring_bias: 2.into(),
}
}
}
impl<N> BackoffAuthoringBlocksStrategy<N> for BackoffAuthoringOnFinalizedHeadLagging<N>
where
N: BaseArithmetic + Copy
{
fn should_backoff(
&self,
chain_head_number: N,
chain_head_slot: u64,
finalized_number: N,
slot_now: u64,
logging_target: &str,
) -> bool {
// This should not happen, but we want to keep the previous behaviour if it does.
if slot_now <= chain_head_slot {
return false;
}
let unfinalized_block_length = chain_head_number - finalized_number;
let interval = unfinalized_block_length.saturating_sub(self.unfinalized_slack)
/ self.authoring_bias;
let interval = interval.min(self.max_interval);
// We're doing arithmetic between block and slot numbers.
let interval: u64 = interval.unique_saturated_into();
// If interval is nonzero we backoff if the current slot isn't far enough ahead of the chain
// head.
if slot_now <= chain_head_slot + interval {
info!(
target: logging_target,
"Backing off claiming new slot for block authorship: finality is lagging.",
);
true
} else {
false
}
}
}
impl<N> BackoffAuthoringBlocksStrategy<N> for () {
fn should_backoff(
&self,
_chain_head_number: N,
_chain_head_slot: u64,
_finalized_number: N,
_slot_now: u64,
_logging_target: &str,
) -> bool {
false
}
}
#[cfg(test)]
mod test {
use std::time::{Duration, Instant};
use crate::{BackoffAuthoringOnFinalizedHeadLagging, BackoffAuthoringBlocksStrategy};
use substrate_test_runtime_client::runtime::Block;
use sp_api::NumberFor;
const SLOT_DURATION: Duration = Duration::from_millis(6000);
@@ -644,4 +762,343 @@ mod test {
Some(SLOT_DURATION * 2u32.pow(7)),
);
}
#[derive(PartialEq, Debug)]
struct HeadState {
head_number: NumberFor<Block>,
head_slot: u64,
slot_now: NumberFor<Block>,
}
impl HeadState {
fn author_block(&mut self) {
// Add a block to the head, and set latest slot to the current
self.head_number += 1;
self.head_slot = self.slot_now;
// Advance slot to next
self.slot_now += 1;
}
fn dont_author_block(&mut self) {
self.slot_now += 1;
}
}
#[test]
fn should_never_backoff_when_head_not_advancing() {
let strategy = BackoffAuthoringOnFinalizedHeadLagging::<NumberFor<Block>> {
max_interval: 100,
unfinalized_slack: 5,
authoring_bias: 2,
};
let head_number = 1;
let head_slot = 1;
let finalized_number = 1;
let slot_now = 2;
let should_backoff: Vec<bool> = (slot_now..1000)
.map(|s| strategy.should_backoff(head_number, head_slot, finalized_number, s, "slots"))
.collect();
// Should always be false, since the head isn't advancing
let expected: Vec<bool> = (slot_now..1000).map(|_| false).collect();
assert_eq!(should_backoff, expected);
}
#[test]
fn should_stop_authoring_if_blocks_are_still_produced_when_finality_stalled() {
let strategy = BackoffAuthoringOnFinalizedHeadLagging::<NumberFor<Block>> {
max_interval: 100,
unfinalized_slack: 5,
authoring_bias: 2,
};
let mut head_number = 1;
let mut head_slot = 1;
let finalized_number = 1;
let slot_now = 2;
let should_backoff: Vec<bool> = (slot_now..300)
.map(move |s| {
let b = strategy.should_backoff(
head_number,
head_slot,
finalized_number,
s,
"slots",
);
// Chain is still advancing (by someone else)
head_number += 1;
head_slot = s;
b
})
.collect();
// Should always be true after a short while, since the chain is advancing but finality is stalled
let expected: Vec<bool> = (slot_now..300).map(|s| s > 8).collect();
assert_eq!(should_backoff, expected);
}
#[test]
fn should_never_backoff_if_max_interval_is_reached() {
let strategy = BackoffAuthoringOnFinalizedHeadLagging::<NumberFor<Block>> {
max_interval: 100,
unfinalized_slack: 5,
authoring_bias: 2,
};
// The limit `max_interval` is used when the unfinalized chain grows to
// `max_interval * authoring_bias + unfinalized_slack`,
// which for the above parameters becomes
// 100 * 2 + 5 = 205.
// Hence we trigger this with head_number > finalized_number + 205.
let head_number = 207;
let finalized_number = 1;
// The limit is then used once the current slot is `max_interval` ahead of slot of the head.
let head_slot = 1;
let slot_now = 2;
let max_interval = strategy.max_interval;
let should_backoff: Vec<bool> = (slot_now..200)
.map(|s| strategy.should_backoff(head_number, head_slot, finalized_number, s, "slots"))
.collect();
// Should backoff (true) until we are `max_interval` number of slots ahead of the chain
// head slot, then we never backoff (false).
let expected: Vec<bool> = (slot_now..200).map(|s| s <= max_interval + head_slot).collect();
assert_eq!(should_backoff, expected);
}
#[test]
fn should_backoff_authoring_when_finality_stalled() {
let param = BackoffAuthoringOnFinalizedHeadLagging {
max_interval: 100,
unfinalized_slack: 5,
authoring_bias: 2,
};
let finalized_number = 2;
let mut head_state = HeadState {
head_number: 4,
head_slot: 10,
slot_now: 11,
};
let should_backoff = |head_state: &HeadState| -> bool {
<dyn BackoffAuthoringBlocksStrategy<NumberFor<Block>>>::should_backoff(
&param,
head_state.head_number,
head_state.head_slot,
finalized_number,
head_state.slot_now,
"slots",
)
};
let backoff: Vec<bool> = (head_state.slot_now..200)
.map(|_| {
if should_backoff(&head_state) {
head_state.dont_author_block();
true
} else {
head_state.author_block();
false
}
})
.collect();
// Gradually start to backoff more and more frequently
let expected = [
false, false, false, false, false, // no effect
true, false,
true, false, // 1:1
true, true, false,
true, true, false, // 2:1
true, true, true, false,
true, true, true, false, // 3:1
true, true, true, true, false,
true, true, true, true, false, // 4:1
true, true, true, true, true, false,
true, true, true, true, true, false, // 5:1
true, true, true, true, true, true, false,
true, true, true, true, true, true, false, // 6:1
true, true, true, true, true, true, true, false,
true, true, true, true, true, true, true, false, // 7:1
true, true, true, true, true, true, true, true, false,
true, true, true, true, true, true, true, true, false, // 8:1
true, true, true, true, true, true, true, true, true, false,
true, true, true, true, true, true, true, true, true, false, // 9:1
true, true, true, true, true, true, true, true, true, true, false,
true, true, true, true, true, true, true, true, true, true, false, // 10:1
true, true, true, true, true, true, true, true, true, true, true, false,
true, true, true, true, true, true, true, true, true, true, true, false, // 11:1
true, true, true, true, true, true, true, true, true, true, true, true, false,
true, true, true, true, true, true, true, true, true, true, true, true, false, // 12:1
true, true, true, true,
];
assert_eq!(backoff, expected);
}
#[test]
fn should_never_wait_more_than_max_interval() {
let param = BackoffAuthoringOnFinalizedHeadLagging {
max_interval: 100,
unfinalized_slack: 5,
authoring_bias: 2,
};
let finalized_number = 2;
let starting_slot = 11;
let mut head_state = HeadState {
head_number: 4,
head_slot: 10,
slot_now: starting_slot,
};
let should_backoff = |head_state: &HeadState| -> bool {
<dyn BackoffAuthoringBlocksStrategy<NumberFor<Block>>>::should_backoff(
&param,
head_state.head_number,
head_state.head_slot,
finalized_number,
head_state.slot_now,
"slots",
)
};
let backoff: Vec<bool> = (head_state.slot_now..40000)
.map(|_| {
if should_backoff(&head_state) {
head_state.dont_author_block();
true
} else {
head_state.author_block();
false
}
})
.collect();
let slots_claimed: Vec<usize> = backoff
.iter()
.enumerate()
.filter(|&(_i, x)| x == &false)
.map(|(i, _x)| i + starting_slot as usize)
.collect();
let last_slot = backoff.len() + starting_slot as usize;
let mut last_two_claimed = slots_claimed.iter().rev().take(2);
// Check that we claimed all the way to the end. Check two slots for when we have an uneven
// number of slots_claimed.
let expected_distance = param.max_interval as usize + 1;
assert_eq!(last_slot - last_two_claimed.next().unwrap(), 92);
assert_eq!(last_slot - last_two_claimed.next().unwrap(), 92 + expected_distance);
let intervals: Vec<_> = slots_claimed
.windows(2)
.map(|x| x[1] - x[0])
.collect();
// The key thing is that the distance between claimed slots is capped to `max_interval + 1`
// assert_eq!(max_observed_interval, Some(&expected_distance));
assert_eq!(intervals.iter().max(), Some(&expected_distance));
// But lets assert all distances, which we expect to grow linearly until `max_interval + 1`
let expected_intervals: Vec<_> = (0..497)
.map(|i| (i/2).max(1).min(expected_distance) )
.collect();
assert_eq!(intervals, expected_intervals);
}
fn run_until_max_interval(param: BackoffAuthoringOnFinalizedHeadLagging<u64>) -> (u64, u64) {
let finalized_number = 0;
let mut head_state = HeadState {
head_number: 0,
head_slot: 0,
slot_now: 1,
};
let should_backoff = |head_state: &HeadState| -> bool {
<dyn BackoffAuthoringBlocksStrategy<NumberFor<Block>>>::should_backoff(
&param,
head_state.head_number,
head_state.head_slot,
finalized_number,
head_state.slot_now,
"slots",
)
};
// Number of blocks until we reach the max interval
let block_for_max_interval
= param.max_interval * param.authoring_bias + param.unfinalized_slack;
while head_state.head_number < block_for_max_interval {
if should_backoff(&head_state) {
head_state.dont_author_block();
} else {
head_state.author_block();
}
}
let slot_time = 6;
let time_to_reach_limit = slot_time * head_state.slot_now;
(block_for_max_interval, time_to_reach_limit)
}
// Denoting
// C: unfinalized_slack
// M: authoring_bias
// X: max_interval
// then the number of slots to reach the max interval can be computed from
// (start_slot + C) + M * sum(n, 1, X)
// or
// (start_slot + C) + M * X*(X+1)/2
fn expected_time_to_reach_max_interval(
param: &BackoffAuthoringOnFinalizedHeadLagging<u64>
) -> (u64, u64) {
let c = param.unfinalized_slack;
let m = param.authoring_bias;
let x = param.max_interval;
let slot_time = 6;
let block_for_max_interval = x * m + c;
// The 1 is because we start at slot_now = 1.
let expected_number_of_slots = (1 + c) + m * x * (x + 1) / 2;
let time_to_reach = expected_number_of_slots * slot_time;
(block_for_max_interval, time_to_reach)
}
#[test]
fn time_to_reach_upper_bound_for_smaller_slack() {
let param = BackoffAuthoringOnFinalizedHeadLagging {
max_interval: 100,
unfinalized_slack: 5,
authoring_bias: 2,
};
let expected = expected_time_to_reach_max_interval(&param);
let (block_for_max_interval, time_to_reach_limit) = run_until_max_interval(param);
assert_eq!((block_for_max_interval, time_to_reach_limit), expected);
// Note: 16 hours is 57600 sec
assert_eq!((block_for_max_interval, time_to_reach_limit), (205, 60636));
}
#[test]
fn time_to_reach_upper_bound_for_larger_slack() {
let param = BackoffAuthoringOnFinalizedHeadLagging {
max_interval: 100,
unfinalized_slack: 50,
authoring_bias: 2,
};
let expected = expected_time_to_reach_max_interval(&param);
let (block_for_max_interval, time_to_reach_limit) = run_until_max_interval(param);
assert_eq!((block_for_max_interval, time_to_reach_limit), expected);
assert_eq!((block_for_max_interval, time_to_reach_limit), (250, 60906));
}
}