#![forbid(unsafe_code)]
#![warn(missing_docs)]
mod aux_schema;
mod slots;
pub use aux_schema::{check_equivocation, MAX_SLOT_CAPACITY, PRUNING_BOUND};
use slots::Slots;
pub use slots::{time_until_next_slot, SlotInfo};
use futures::{future::Either, Future, TryFutureExt};
use futures_timer::Delay;
use log::{debug, info, warn};
use sc_consensus::{BlockImport, JustificationSyncLink};
use sc_telemetry::{telemetry, TelemetryHandle, CONSENSUS_DEBUG, CONSENSUS_INFO, CONSENSUS_WARN};
use sp_arithmetic::traits::BaseArithmetic;
use sp_consensus::{Proposal, Proposer, SelectChain, SyncOracle};
use sp_consensus_slots::{Slot, SlotDuration};
use sp_inherents::CreateInherentDataProviders;
use sp_runtime::traits::{Block as BlockT, HashingFor, Header as HeaderT};
use std::{
fmt::Debug,
ops::Deref,
time::{Duration, Instant},
};
const LOG_TARGET: &str = "slots";
pub type StorageChanges<Block> = sp_state_machine::StorageChanges<HashingFor<Block>>;
#[derive(Debug, Clone)]
pub struct SlotResult<Block: BlockT, Proof> {
pub block: Block,
pub storage_proof: Proof,
}
#[async_trait::async_trait]
pub trait SlotWorker<B: BlockT, Proof> {
async fn on_slot(&mut self, slot_info: SlotInfo<B>) -> Option<SlotResult<B, Proof>>;
}
#[async_trait::async_trait]
pub trait SimpleSlotWorker<B: BlockT> {
type BlockImport: BlockImport<B> + Send + 'static;
type SyncOracle: SyncOracle;
type JustificationSyncLink: JustificationSyncLink<B>;
type CreateProposer: Future<Output = Result<Self::Proposer, sp_consensus::Error>>
+ Send
+ Unpin
+ 'static;
type Proposer: Proposer<B> + Send;
type Claim: Send + Sync + 'static;
type AuxData: Send + Sync + 'static;
fn logging_target(&self) -> &'static str;
fn block_import(&mut self) -> &mut Self::BlockImport;
fn aux_data(
&self,
header: &B::Header,
slot: Slot,
) -> Result<Self::AuxData, sp_consensus::Error>;
fn authorities_len(&self, aux_data: &Self::AuxData) -> Option<usize>;
async fn claim_slot(
&mut self,
header: &B::Header,
slot: Slot,
aux_data: &Self::AuxData,
) -> Option<Self::Claim>;
fn notify_slot(&self, _header: &B::Header, _slot: Slot, _aux_data: &Self::AuxData) {}
fn pre_digest_data(&self, slot: Slot, claim: &Self::Claim) -> Vec<sp_runtime::DigestItem>;
async fn block_import_params(
&self,
header: B::Header,
header_hash: &B::Hash,
body: Vec<B::Extrinsic>,
storage_changes: StorageChanges<B>,
public: Self::Claim,
aux_data: Self::AuxData,
) -> Result<sc_consensus::BlockImportParams<B>, sp_consensus::Error>;
fn force_authoring(&self) -> bool;
fn should_backoff(&self, _slot: Slot, _chain_head: &B::Header) -> bool {
false
}
fn sync_oracle(&mut self) -> &mut Self::SyncOracle;
fn justification_sync_link(&mut self) -> &mut Self::JustificationSyncLink;
fn proposer(&mut self, block: &B::Header) -> Self::CreateProposer;
fn telemetry(&self) -> Option<TelemetryHandle>;
fn proposing_remaining_duration(&self, slot_info: &SlotInfo<B>) -> Duration;
async fn propose(
&mut self,
proposer: Self::Proposer,
claim: &Self::Claim,
slot_info: SlotInfo<B>,
end_proposing_at: Instant,
) -> Option<Proposal<B, <Self::Proposer as Proposer<B>>::Proof>> {
let slot = slot_info.slot;
let telemetry = self.telemetry();
let log_target = self.logging_target();
let inherent_data =
Self::create_inherent_data(&slot_info, &log_target, end_proposing_at).await?;
let proposing_remaining_duration =
end_proposing_at.saturating_duration_since(Instant::now());
let logs = self.pre_digest_data(slot, claim);
let proposing = proposer
.propose(
inherent_data,
sp_runtime::generic::Digest { logs },
proposing_remaining_duration.mul_f32(0.98),
slot_info.block_size_limit,
)
.map_err(|e| sp_consensus::Error::ClientImport(e.to_string()));
let proposal = match futures::future::select(
proposing,
Delay::new(proposing_remaining_duration),
)
.await
{
Either::Left((Ok(p), _)) => p,
Either::Left((Err(err), _)) => {
warn!(target: log_target, "Proposing failed: {}", err);
return None
},
Either::Right(_) => {
info!(
target: log_target,
"โ๏ธ Discarding proposal for slot {}; block production took too long", slot,
);
#[cfg(build_profile = "debug")]
info!(
target: log_target,
"๐ Recompile your node in `--release` mode to mitigate this problem.",
);
telemetry!(
telemetry;
CONSENSUS_INFO;
"slots.discarding_proposal_took_too_long";
"slot" => *slot,
);
return None
},
};
Some(proposal)
}
async fn create_inherent_data(
slot_info: &SlotInfo<B>,
logging_target: &str,
end_proposing_at: Instant,
) -> Option<sp_inherents::InherentData> {
let remaining_duration = end_proposing_at.saturating_duration_since(Instant::now());
let delay = Delay::new(remaining_duration);
let cid = slot_info.create_inherent_data.create_inherent_data();
let inherent_data = match futures::future::select(delay, cid).await {
Either::Right((Ok(data), _)) => data,
Either::Right((Err(err), _)) => {
warn!(
target: logging_target,
"Unable to create inherent data for block {:?}: {}",
slot_info.chain_head.hash(),
err,
);
return None
},
Either::Left(_) => {
warn!(
target: logging_target,
"Creating inherent data took more time than we had left for slot {} for block {:?}.",
slot_info.slot,
slot_info.chain_head.hash(),
);
return None
},
};
Some(inherent_data)
}
async fn on_slot(
&mut self,
slot_info: SlotInfo<B>,
) -> Option<SlotResult<B, <Self::Proposer as Proposer<B>>::Proof>>
where
Self: Sync,
{
let slot = slot_info.slot;
let telemetry = self.telemetry();
let logging_target = self.logging_target();
let proposing_remaining_duration = self.proposing_remaining_duration(&slot_info);
let end_proposing_at = if proposing_remaining_duration == Duration::default() {
debug!(
target: logging_target,
"Skipping proposal slot {} since there's no time left to propose", slot,
);
return None
} else {
Instant::now() + proposing_remaining_duration
};
let aux_data = match self.aux_data(&slot_info.chain_head, slot) {
Ok(aux_data) => aux_data,
Err(err) => {
warn!(
target: logging_target,
"Unable to fetch auxiliary data for block {:?}: {}",
slot_info.chain_head.hash(),
err,
);
telemetry!(
telemetry;
CONSENSUS_WARN;
"slots.unable_fetching_authorities";
"slot" => ?slot_info.chain_head.hash(),
"err" => ?err,
);
return None
},
};
self.notify_slot(&slot_info.chain_head, slot, &aux_data);
let authorities_len = self.authorities_len(&aux_data);
if !self.force_authoring() &&
self.sync_oracle().is_offline() &&
authorities_len.map(|a| a > 1).unwrap_or(false)
{
debug!(target: logging_target, "Skipping proposal slot. Waiting for the network.");
telemetry!(
telemetry;
CONSENSUS_DEBUG;
"slots.skipping_proposal_slot";
"authorities_len" => authorities_len,
);
return None
}
let claim = self.claim_slot(&slot_info.chain_head, slot, &aux_data).await?;
if self.should_backoff(slot, &slot_info.chain_head) {
return None
}
debug!(target: logging_target, "Starting authorship at slot: {slot}");
telemetry!(telemetry; CONSENSUS_DEBUG; "slots.starting_authorship"; "slot_num" => slot);
let proposer = match self.proposer(&slot_info.chain_head).await {
Ok(p) => p,
Err(err) => {
warn!(target: logging_target, "Unable to author block in slot {slot:?}: {err}");
telemetry!(
telemetry;
CONSENSUS_WARN;
"slots.unable_authoring_block";
"slot" => *slot,
"err" => ?err
);
return None
},
};
let proposal = self.propose(proposer, &claim, slot_info, end_proposing_at).await?;
let (block, storage_proof) = (proposal.block, proposal.proof);
let (header, body) = block.deconstruct();
let header_num = *header.number();
let header_hash = header.hash();
let parent_hash = *header.parent_hash();
let block_import_params = match self
.block_import_params(
header,
&header_hash,
body.clone(),
proposal.storage_changes,
claim,
aux_data,
)
.await
{
Ok(bi) => bi,
Err(err) => {
warn!(target: logging_target, "Failed to create block import params: {}", err);
return None
},
};
info!(
target: logging_target,
"๐ Pre-sealed block for proposal at {}. Hash now {:?}, previously {:?}.",
header_num,
block_import_params.post_hash(),
header_hash,
);
telemetry!(
telemetry;
CONSENSUS_INFO;
"slots.pre_sealed_block";
"header_num" => ?header_num,
"hash_now" => ?block_import_params.post_hash(),
"hash_previously" => ?header_hash,
);
let header = block_import_params.post_header();
match self.block_import().import_block(block_import_params).await {
Ok(res) => {
res.handle_justification(
&header.hash(),
*header.number(),
self.justification_sync_link(),
);
},
Err(err) => {
warn!(
target: logging_target,
"Error with block built on {:?}: {}", parent_hash, err,
);
telemetry!(
telemetry;
CONSENSUS_WARN;
"slots.err_with_block_built_on";
"hash" => ?parent_hash,
"err" => ?err,
);
},
}
Some(SlotResult { block: B::new(header, body), storage_proof })
}
}
pub struct SimpleSlotWorkerToSlotWorker<T>(pub T);
#[async_trait::async_trait]
impl<T: SimpleSlotWorker<B> + Send + Sync, B: BlockT>
SlotWorker<B, <T::Proposer as Proposer<B>>::Proof> for SimpleSlotWorkerToSlotWorker<T>
{
async fn on_slot(
&mut self,
slot_info: SlotInfo<B>,
) -> Option<SlotResult<B, <T::Proposer as Proposer<B>>::Proof>> {
self.0.on_slot(slot_info).await
}
}
pub trait InherentDataProviderExt {
fn slot(&self) -> Slot;
}
macro_rules! impl_inherent_data_provider_ext_tuple {
( S $(, $TN:ident)* $( , )?) => {
impl<S, $( $TN ),*> InherentDataProviderExt for (S, $($TN),*)
where
S: Deref<Target = Slot>,
{
fn slot(&self) -> Slot {
*self.0.deref()
}
}
}
}
impl_inherent_data_provider_ext_tuple!(S);
impl_inherent_data_provider_ext_tuple!(S, A);
impl_inherent_data_provider_ext_tuple!(S, A, B);
impl_inherent_data_provider_ext_tuple!(S, A, B, C);
impl_inherent_data_provider_ext_tuple!(S, A, B, C, D);
impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E);
impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F);
impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F, G);
impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F, G, H);
impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F, G, H, I);
impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F, G, H, I, J);
pub async fn start_slot_worker<B, C, W, SO, CIDP, Proof>(
slot_duration: SlotDuration,
client: C,
mut worker: W,
sync_oracle: SO,
create_inherent_data_providers: CIDP,
) where
B: BlockT,
C: SelectChain<B>,
W: SlotWorker<B, Proof>,
SO: SyncOracle + Send,
CIDP: CreateInherentDataProviders<B, ()> + Send + 'static,
CIDP::InherentDataProviders: InherentDataProviderExt + Send,
{
let mut slots = Slots::new(
slot_duration.as_duration(),
create_inherent_data_providers,
client,
sync_oracle,
);
loop {
let slot_info = slots.next_slot().await;
let _ = worker.on_slot(slot_info).await;
}
}
pub enum CheckedHeader<H, S> {
Deferred(H, Slot),
Checked(H, S),
}
pub struct SlotProportion(f32);
impl SlotProportion {
pub fn new(inner: f32) -> Self {
Self(inner.clamp(0.0, 1.0))
}
pub fn get(&self) -> f32 {
self.0
}
}
pub enum SlotLenienceType {
Linear,
Exponential,
}
impl SlotLenienceType {
fn as_str(&self) -> &'static str {
match self {
SlotLenienceType::Linear => "linear",
SlotLenienceType::Exponential => "exponential",
}
}
}
pub fn proposing_remaining_duration<Block: BlockT>(
parent_slot: Option<Slot>,
slot_info: &SlotInfo<Block>,
block_proposal_slot_portion: &SlotProportion,
max_block_proposal_slot_portion: Option<&SlotProportion>,
slot_lenience_type: SlotLenienceType,
log_target: &str,
) -> Duration {
use sp_runtime::traits::Zero;
let proposing_duration = slot_info.duration.mul_f32(block_proposal_slot_portion.get());
let slot_remaining = slot_info
.ends_at
.checked_duration_since(std::time::Instant::now())
.unwrap_or_default();
let proposing_duration = std::cmp::min(slot_remaining, proposing_duration);
if slot_info.chain_head.number().is_zero() {
return proposing_duration
}
let parent_slot = match parent_slot {
Some(parent_slot) => parent_slot,
None => return proposing_duration,
};
let slot_lenience = match slot_lenience_type {
SlotLenienceType::Exponential => slot_lenience_exponential(parent_slot, slot_info),
SlotLenienceType::Linear => slot_lenience_linear(parent_slot, slot_info),
};
if let Some(slot_lenience) = slot_lenience {
let lenient_proposing_duration =
proposing_duration + slot_lenience.mul_f32(block_proposal_slot_portion.get());
let lenient_proposing_duration =
if let Some(max_block_proposal_slot_portion) = max_block_proposal_slot_portion {
std::cmp::min(
lenient_proposing_duration,
slot_info.duration.mul_f32(max_block_proposal_slot_portion.get()),
)
} else {
lenient_proposing_duration
};
debug!(
target: log_target,
"No block for {} slots. Applying {} lenience, total proposing duration: {}ms",
slot_info.slot.saturating_sub(parent_slot + 1),
slot_lenience_type.as_str(),
lenient_proposing_duration.as_millis(),
);
lenient_proposing_duration
} else {
proposing_duration
}
}
pub fn slot_lenience_exponential<Block: BlockT>(
parent_slot: Slot,
slot_info: &SlotInfo<Block>,
) -> Option<Duration> {
const BACKOFF_CAP: u64 = 7;
const BACKOFF_STEP: u64 = 2;
let skipped_slots = *slot_info.slot.saturating_sub(parent_slot + 1);
if skipped_slots == 0 {
None
} else {
let slot_lenience = skipped_slots / BACKOFF_STEP;
let slot_lenience = std::cmp::min(slot_lenience, BACKOFF_CAP);
let slot_lenience = 1 << slot_lenience;
Some(slot_lenience * slot_info.duration)
}
}
pub fn slot_lenience_linear<Block: BlockT>(
parent_slot: Slot,
slot_info: &SlotInfo<Block>,
) -> Option<Duration> {
const BACKOFF_CAP: u64 = 20;
let skipped_slots = *slot_info.slot.saturating_sub(parent_slot + 1);
if skipped_slots == 0 {
None
} else {
let slot_lenience = std::cmp::min(skipped_slots, BACKOFF_CAP);
Some(slot_info.duration * (slot_lenience as u32))
}
}
pub trait BackoffAuthoringBlocksStrategy<N> {
fn should_backoff(
&self,
chain_head_number: N,
chain_head_slot: Slot,
finalized_number: N,
slow_now: Slot,
logging_target: &str,
) -> bool;
}
#[derive(Clone)]
pub struct BackoffAuthoringOnFinalizedHeadLagging<N> {
pub max_interval: N,
pub unfinalized_slack: N,
pub authoring_bias: N,
}
impl<N: BaseArithmetic> Default for BackoffAuthoringOnFinalizedHeadLagging<N> {
fn default() -> Self {
Self {
max_interval: 100.into(),
unfinalized_slack: 50.into(),
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: Slot,
finalized_number: N,
slot_now: Slot,
logging_target: &str,
) -> bool {
if slot_now <= chain_head_slot {
return false
}
let unfinalized_block_length = chain_head_number.saturating_sub(finalized_number);
let interval =
unfinalized_block_length.saturating_sub(self.unfinalized_slack) / self.authoring_bias;
let interval = interval.min(self.max_interval);
let interval: u64 = interval.unique_saturated_into();
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: Slot,
_finalized_number: N,
_slot_now: Slot,
_logging_target: &str,
) -> bool {
false
}
}
#[cfg(test)]
mod test {
use super::*;
use sp_runtime::traits::NumberFor;
use std::time::{Duration, Instant};
use substrate_test_runtime_client::runtime::{Block, Header};
const SLOT_DURATION: Duration = Duration::from_millis(6000);
fn slot(slot: u64) -> super::slots::SlotInfo<Block> {
super::slots::SlotInfo {
slot: slot.into(),
duration: SLOT_DURATION,
create_inherent_data: Box::new(()),
ends_at: Instant::now() + SLOT_DURATION,
chain_head: Header::new(
1,
Default::default(),
Default::default(),
Default::default(),
Default::default(),
),
block_size_limit: None,
}
}
#[test]
fn linear_slot_lenience() {
assert_eq!(super::slot_lenience_linear(1u64.into(), &slot(2)), None);
for n in 3..=22 {
assert_eq!(
super::slot_lenience_linear(1u64.into(), &slot(n)),
Some(SLOT_DURATION * (n - 2) as u32),
);
}
assert_eq!(super::slot_lenience_linear(1u64.into(), &slot(23)), Some(SLOT_DURATION * 20));
}
#[test]
fn exponential_slot_lenience() {
assert_eq!(super::slot_lenience_exponential(1u64.into(), &slot(2)), None);
for n in 3..=17 {
assert_eq!(
super::slot_lenience_exponential(1u64.into(), &slot(n)),
Some(SLOT_DURATION * 2u32.pow((n / 2 - 1) as u32)),
);
}
assert_eq!(
super::slot_lenience_exponential(1u64.into(), &slot(18)),
Some(SLOT_DURATION * 2u32.pow(7)),
);
assert_eq!(
super::slot_lenience_exponential(1u64.into(), &slot(19)),
Some(SLOT_DURATION * 2u32.pow(7)),
);
}
#[test]
fn proposing_remaining_duration_should_apply_lenience_based_on_proposal_slot_proportion() {
assert_eq!(
proposing_remaining_duration(
Some(0.into()),
&slot(2),
&SlotProportion(0.25),
None,
SlotLenienceType::Linear,
"test",
),
SLOT_DURATION.mul_f32(0.25 * 2.0),
);
}
#[test]
fn proposing_remaining_duration_should_never_exceed_max_proposal_slot_proportion() {
assert_eq!(
proposing_remaining_duration(
Some(0.into()),
&slot(100),
&SlotProportion(0.25),
Some(SlotProportion(0.9)).as_ref(),
SlotLenienceType::Exponential,
"test",
),
SLOT_DURATION.mul_f32(0.9),
);
}
#[derive(PartialEq, Debug)]
struct HeadState {
head_number: NumberFor<Block>,
head_slot: u64,
slot_now: NumberFor<Block>,
}
impl HeadState {
fn author_block(&mut self) {
self.head_number += 1;
self.head_slot = self.slot_now;
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.into(),
finalized_number,
s.into(),
"slots",
)
})
.collect();
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.into(),
finalized_number,
s.into(),
"slots",
);
head_number += 1;
head_slot = s;
b
})
.collect();
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,
};
let head_number = 207;
let finalized_number = 1;
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.into(),
finalized_number,
s.into(),
"slots",
)
})
.collect();
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(
¶m,
head_state.head_number,
head_state.head_slot.into(),
finalized_number,
head_state.slot_now.into(),
"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();
let expected = [
false, false, false, false, false, true, false, true, false, true, true, false, true, true, false, true, true, true, false, true, true, true, false, true, true, true, true, false, true, true, true, true, false, true, true, true, true, true, false, true, true, true, true, true, false, true, true, true, true, true, true, false, true, true, true, true, true, true,
false, true, true, true, true, true, true, true, false, true, true, true, true, true, true,
true, false, true, true, true, true, true, true, true, true, false, true, true, true, true, true,
true, true, true, false, true, true, true, true, true, true, true, true, true, false, true, true, true, true,
true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true, false, true, true, true,
true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true, true, false, 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, true,
true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true,
];
assert_eq!(backoff.as_slice(), &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(
¶m,
head_state.head_number,
head_state.head_slot.into(),
finalized_number,
head_state.slot_now.into(),
"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);
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();
assert_eq!(intervals.iter().max(), Some(&expected_distance));
let expected_intervals: Vec<_> =
(0..497).map(|i| (i / 2).clamp(1, 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(
¶m,
head_state.head_number,
head_state.head_slot.into(),
finalized_number,
head_state.slot_now.into(),
"slots",
)
};
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)
}
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;
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(¶m);
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), (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(¶m);
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));
}
}