Skip to main content

pezsc_consensus_slots/
lib.rs

1// This file is part of Bizinikiwi.
2
3// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19//! Slots functionality for Bizinikiwi.
20//!
21//! Some consensus algorithms have a concept of *slots*, which are intervals in
22//! time during which certain events can and/or must occur.  This crate
23//! provides generic functionality for slots.
24
25#![forbid(unsafe_code)]
26#![warn(missing_docs)]
27
28mod aux_schema;
29mod slots;
30
31pub use aux_schema::{check_equivocation, MAX_SLOT_CAPACITY, PRUNING_BOUND};
32use slots::Slots;
33pub use slots::{time_until_next_slot, SlotInfo};
34
35use futures::{future::Either, Future, TryFutureExt};
36use futures_timer::Delay;
37use log::{debug, info, warn};
38use pezsc_consensus::{BlockImport, JustificationSyncLink};
39use pezsc_telemetry::{
40	telemetry, TelemetryHandle, CONSENSUS_DEBUG, CONSENSUS_INFO, CONSENSUS_WARN,
41};
42use pezsp_arithmetic::traits::BaseArithmetic;
43use pezsp_consensus::{Proposal, Proposer, SelectChain, SyncOracle};
44use pezsp_consensus_slots::{Slot, SlotDuration};
45use pezsp_inherents::CreateInherentDataProviders;
46use pezsp_runtime::traits::{Block as BlockT, HashingFor, Header as HeaderT};
47use std::{
48	fmt::Debug,
49	ops::Deref,
50	time::{Duration, Instant},
51};
52
53const LOG_TARGET: &str = "slots";
54
55/// The changes that need to applied to the storage to create the state for a block.
56///
57/// See [`pezsp_state_machine::StorageChanges`] for more information.
58pub type StorageChanges<Block> = pezsp_state_machine::StorageChanges<HashingFor<Block>>;
59
60/// The result of [`SlotWorker::on_slot`].
61#[derive(Debug, Clone)]
62pub struct SlotResult<Block: BlockT, Proof> {
63	/// The block that was built.
64	pub block: Block,
65	/// The storage proof that was recorded while building the block.
66	pub storage_proof: Proof,
67}
68
69/// A worker that should be invoked at every new slot.
70///
71/// The implementation should not make any assumptions of the slot being bound to the time or
72/// similar. The only valid assumption is that the slot number is always increasing.
73#[async_trait::async_trait]
74pub trait SlotWorker<B: BlockT, Proof> {
75	/// Called when a new slot is triggered.
76	///
77	/// Returns a future that resolves to a [`SlotResult`] iff a block was successfully built in
78	/// the slot. Otherwise `None` is returned.
79	async fn on_slot(&mut self, slot_info: SlotInfo<B>) -> Option<SlotResult<B, Proof>>;
80}
81
82/// A skeleton implementation for `SlotWorker` which tries to claim a slot at
83/// its beginning and tries to produce a block if successfully claimed, timing
84/// out if block production takes too long.
85#[async_trait::async_trait]
86pub trait SimpleSlotWorker<B: BlockT> {
87	/// A handle to a `BlockImport`.
88	type BlockImport: BlockImport<B> + Send + 'static;
89
90	/// A handle to a `SyncOracle`.
91	type SyncOracle: SyncOracle;
92
93	/// A handle to a `JustificationSyncLink`, allows hooking into the sync module to control the
94	/// justification sync process.
95	type JustificationSyncLink: JustificationSyncLink<B>;
96
97	/// The type of future resolving to the proposer.
98	type CreateProposer: Future<Output = Result<Self::Proposer, pezsp_consensus::Error>>
99		+ Send
100		+ Unpin
101		+ 'static;
102
103	/// The type of proposer to use to build blocks.
104	type Proposer: Proposer<B> + Send;
105
106	/// Data associated with a slot claim.
107	type Claim: Send + Sync + 'static;
108
109	/// Auxiliary data necessary for authoring.
110	type AuxData: Send + Sync + 'static;
111
112	/// The logging target to use when logging messages.
113	fn logging_target(&self) -> &'static str;
114
115	/// A handle to a `BlockImport`.
116	fn block_import(&mut self) -> &mut Self::BlockImport;
117
118	/// Returns the auxiliary data necessary for authoring.
119	fn aux_data(
120		&self,
121		header: &B::Header,
122		slot: Slot,
123	) -> Result<Self::AuxData, pezsp_consensus::Error>;
124
125	/// Returns the number of authorities.
126	/// None indicate that the authorities information is incomplete.
127	fn authorities_len(&self, aux_data: &Self::AuxData) -> Option<usize>;
128
129	/// Tries to claim the given slot, returning an object with claim data if successful.
130	async fn claim_slot(
131		&mut self,
132		header: &B::Header,
133		slot: Slot,
134		aux_data: &Self::AuxData,
135	) -> Option<Self::Claim>;
136
137	/// Notifies the given slot. Similar to `claim_slot`, but will be called no matter whether we
138	/// need to author blocks or not.
139	fn notify_slot(&self, _header: &B::Header, _slot: Slot, _aux_data: &Self::AuxData) {}
140
141	/// Return the pre digest data to include in a block authored with the given claim.
142	fn pre_digest_data(&self, slot: Slot, claim: &Self::Claim) -> Vec<pezsp_runtime::DigestItem>;
143
144	/// Returns a function which produces a `BlockImportParams`.
145	async fn block_import_params(
146		&self,
147		header: B::Header,
148		header_hash: &B::Hash,
149		body: Vec<B::Extrinsic>,
150		storage_changes: StorageChanges<B>,
151		public: Self::Claim,
152		aux_data: Self::AuxData,
153	) -> Result<pezsc_consensus::BlockImportParams<B>, pezsp_consensus::Error>;
154
155	/// Whether to force authoring if offline.
156	fn force_authoring(&self) -> bool;
157
158	/// Returns whether the block production should back off.
159	///
160	/// By default this function always returns `false`.
161	///
162	/// An example strategy that back offs if the finalized head is lagging too much behind the tip
163	/// is implemented by [`BackoffAuthoringOnFinalizedHeadLagging`].
164	fn should_backoff(&self, _slot: Slot, _chain_head: &B::Header) -> bool {
165		false
166	}
167
168	/// Returns a handle to a `SyncOracle`.
169	fn sync_oracle(&mut self) -> &mut Self::SyncOracle;
170
171	/// Returns a handle to a `JustificationSyncLink`.
172	fn justification_sync_link(&mut self) -> &mut Self::JustificationSyncLink;
173
174	/// Returns a `Proposer` to author on top of the given block.
175	fn proposer(&mut self, block: &B::Header) -> Self::CreateProposer;
176
177	/// Returns a [`TelemetryHandle`] if any.
178	fn telemetry(&self) -> Option<TelemetryHandle>;
179
180	/// Remaining duration for proposing.
181	fn proposing_remaining_duration(&self, slot_info: &SlotInfo<B>) -> Duration;
182
183	/// Propose a block by `Proposer`.
184	async fn propose(
185		&mut self,
186		proposer: Self::Proposer,
187		claim: &Self::Claim,
188		slot_info: SlotInfo<B>,
189		end_proposing_at: Instant,
190	) -> Option<Proposal<B, <Self::Proposer as Proposer<B>>::Proof>> {
191		let slot = slot_info.slot;
192		let telemetry = self.telemetry();
193		let log_target = self.logging_target();
194
195		let inherent_data =
196			Self::create_inherent_data(&slot_info, &log_target, end_proposing_at).await?;
197
198		let proposing_remaining_duration =
199			end_proposing_at.saturating_duration_since(Instant::now());
200		let logs = self.pre_digest_data(slot, claim);
201
202		// deadline our production to 98% of the total time left for proposing. As we deadline
203		// the proposing below to the same total time left, the 2% margin should be enough for
204		// the result to be returned.
205		let proposing = proposer
206			.propose(
207				inherent_data,
208				pezsp_runtime::generic::Digest { logs },
209				proposing_remaining_duration.mul_f32(0.98),
210				slot_info.block_size_limit,
211			)
212			.map_err(|e| pezsp_consensus::Error::ClientImport(e.to_string()));
213
214		let proposal = match futures::future::select(
215			proposing,
216			Delay::new(proposing_remaining_duration),
217		)
218		.await
219		{
220			Either::Left((Ok(p), _)) => p,
221			Either::Left((Err(err), _)) => {
222				warn!(target: log_target, "Proposing failed: {}", err);
223
224				return None;
225			},
226			Either::Right(_) => {
227				info!(
228					target: log_target,
229					"⌛️ Discarding proposal for slot {}; block production took too long", slot,
230				);
231				// If the node was compiled with debug, tell the user to use release optimizations.
232				#[cfg(build_profile = "debug")]
233				info!(
234					target: log_target,
235					"👉 Recompile your node in `--release` mode to mitigate this problem.",
236				);
237				telemetry!(
238					telemetry;
239					CONSENSUS_INFO;
240					"slots.discarding_proposal_took_too_long";
241					"slot" => *slot,
242				);
243
244				return None;
245			},
246		};
247
248		Some(proposal)
249	}
250
251	/// Calls `create_inherent_data` and handles errors.
252	async fn create_inherent_data(
253		slot_info: &SlotInfo<B>,
254		logging_target: &str,
255		end_proposing_at: Instant,
256	) -> Option<pezsp_inherents::InherentData> {
257		let remaining_duration = end_proposing_at.saturating_duration_since(Instant::now());
258		let delay = Delay::new(remaining_duration);
259		let cid = slot_info.create_inherent_data.create_inherent_data();
260		let inherent_data = match futures::future::select(delay, cid).await {
261			Either::Right((Ok(data), _)) => data,
262			Either::Right((Err(err), _)) => {
263				warn!(
264					target: logging_target,
265					"Unable to create inherent data for block {:?}: {}",
266					slot_info.chain_head.hash(),
267					err,
268				);
269
270				return None;
271			},
272			Either::Left(_) => {
273				warn!(
274					target: logging_target,
275					"Creating inherent data took more time than we had left for slot {} for block {:?}.",
276					slot_info.slot,
277					slot_info.chain_head.hash(),
278				);
279
280				return None;
281			},
282		};
283
284		Some(inherent_data)
285	}
286
287	/// Implements [`SlotWorker::on_slot`].
288	async fn on_slot(
289		&mut self,
290		slot_info: SlotInfo<B>,
291	) -> Option<SlotResult<B, <Self::Proposer as Proposer<B>>::Proof>>
292	where
293		Self: Sync,
294	{
295		let slot = slot_info.slot;
296		let telemetry = self.telemetry();
297		let logging_target = self.logging_target();
298
299		let proposing_remaining_duration = self.proposing_remaining_duration(&slot_info);
300
301		let end_proposing_at = if proposing_remaining_duration == Duration::default() {
302			debug!(
303				target: logging_target,
304				"Skipping proposal slot {} since there's no time left to propose", slot,
305			);
306
307			return None;
308		} else {
309			Instant::now() + proposing_remaining_duration
310		};
311
312		let aux_data = match self.aux_data(&slot_info.chain_head, slot) {
313			Ok(aux_data) => aux_data,
314			Err(err) => {
315				warn!(
316					target: logging_target,
317					"Unable to fetch auxiliary data for block {:?}: {}",
318					slot_info.chain_head.hash(),
319					err,
320				);
321
322				telemetry!(
323					telemetry;
324					CONSENSUS_WARN;
325					"slots.unable_fetching_authorities";
326					"slot" => ?slot_info.chain_head.hash(),
327					"err" => ?err,
328				);
329
330				return None;
331			},
332		};
333
334		self.notify_slot(&slot_info.chain_head, slot, &aux_data);
335
336		let authorities_len = self.authorities_len(&aux_data);
337
338		if !self.force_authoring()
339			&& self.sync_oracle().is_offline()
340			&& authorities_len.map(|a| a > 1).unwrap_or(false)
341		{
342			debug!(target: logging_target, "Skipping proposal slot. Waiting for the network.");
343			telemetry!(
344				telemetry;
345				CONSENSUS_DEBUG;
346				"slots.skipping_proposal_slot";
347				"authorities_len" => authorities_len,
348			);
349
350			return None;
351		}
352
353		let claim = self.claim_slot(&slot_info.chain_head, slot, &aux_data).await?;
354
355		if self.should_backoff(slot, &slot_info.chain_head) {
356			return None;
357		}
358
359		debug!(target: logging_target, "Starting authorship at slot: {slot}");
360
361		telemetry!(telemetry; CONSENSUS_DEBUG; "slots.starting_authorship"; "slot_num" => slot);
362
363		let proposer = match self.proposer(&slot_info.chain_head).await {
364			Ok(p) => p,
365			Err(err) => {
366				warn!(target: logging_target, "Unable to author block in slot {slot:?}: {err}");
367
368				telemetry!(
369					telemetry;
370					CONSENSUS_WARN;
371					"slots.unable_authoring_block";
372					"slot" => *slot,
373					"err" => ?err
374				);
375
376				return None;
377			},
378		};
379
380		let proposal = self.propose(proposer, &claim, slot_info, end_proposing_at).await?;
381
382		let (block, storage_proof) = (proposal.block, proposal.proof);
383		let (header, body) = block.deconstruct();
384		let header_num = *header.number();
385		let header_hash = header.hash();
386		let parent_hash = *header.parent_hash();
387
388		let block_import_params = match self
389			.block_import_params(
390				header,
391				&header_hash,
392				body.clone(),
393				proposal.storage_changes,
394				claim,
395				aux_data,
396			)
397			.await
398		{
399			Ok(bi) => bi,
400			Err(err) => {
401				warn!(target: logging_target, "Failed to create block import params: {}", err);
402
403				return None;
404			},
405		};
406
407		info!(
408			target: logging_target,
409			"🔖 Pre-sealed block for proposal at {}. Hash now {:?}, previously {:?}.",
410			header_num,
411			block_import_params.post_hash(),
412			header_hash,
413		);
414
415		telemetry!(
416			telemetry;
417			CONSENSUS_INFO;
418			"slots.pre_sealed_block";
419			"header_num" => ?header_num,
420			"hash_now" => ?block_import_params.post_hash(),
421			"hash_previously" => ?header_hash,
422		);
423
424		let header = block_import_params.post_header();
425		match self.block_import().import_block(block_import_params).await {
426			Ok(res) => {
427				res.handle_justification(
428					&header.hash(),
429					*header.number(),
430					self.justification_sync_link(),
431				);
432			},
433			Err(err) => {
434				warn!(
435					target: logging_target,
436					"Error with block built on {:?}: {}", parent_hash, err,
437				);
438
439				telemetry!(
440					telemetry;
441					CONSENSUS_WARN;
442					"slots.err_with_block_built_on";
443					"hash" => ?parent_hash,
444					"err" => ?err,
445				);
446			},
447		}
448
449		Some(SlotResult { block: B::new(header, body), storage_proof })
450	}
451}
452
453/// A type that implements [`SlotWorker`] for a type that implements [`SimpleSlotWorker`].
454///
455/// This is basically a workaround for Rust not supporting specialization. Otherwise we could
456/// implement [`SlotWorker`] for any `T` that implements [`SimpleSlotWorker`], but currently
457/// that would prevent downstream users to implement [`SlotWorker`] for their own types.
458pub struct SimpleSlotWorkerToSlotWorker<T>(pub T);
459
460#[async_trait::async_trait]
461impl<T: SimpleSlotWorker<B> + Send + Sync, B: BlockT>
462	SlotWorker<B, <T::Proposer as Proposer<B>>::Proof> for SimpleSlotWorkerToSlotWorker<T>
463{
464	async fn on_slot(
465		&mut self,
466		slot_info: SlotInfo<B>,
467	) -> Option<SlotResult<B, <T::Proposer as Proposer<B>>::Proof>> {
468		self.0.on_slot(slot_info).await
469	}
470}
471
472/// Slot specific extension that the inherent data provider needs to implement.
473pub trait InherentDataProviderExt {
474	/// The current slot that will be found in the
475	/// [`InherentData`](`pezsp_inherents::InherentData`).
476	fn slot(&self) -> Slot;
477}
478
479/// Small macro for implementing `InherentDataProviderExt` for inherent data provider tuple.
480macro_rules! impl_inherent_data_provider_ext_tuple {
481	( S $(, $TN:ident)* $( , )?) => {
482		impl<S, $( $TN ),*>  InherentDataProviderExt for (S, $($TN),*)
483		where
484			S: Deref<Target = Slot>,
485		{
486			fn slot(&self) -> Slot {
487				*self.0.deref()
488			}
489		}
490	}
491}
492
493impl_inherent_data_provider_ext_tuple!(S);
494impl_inherent_data_provider_ext_tuple!(S, A);
495impl_inherent_data_provider_ext_tuple!(S, A, B);
496impl_inherent_data_provider_ext_tuple!(S, A, B, C);
497impl_inherent_data_provider_ext_tuple!(S, A, B, C, D);
498impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E);
499impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F);
500impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F, G);
501impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F, G, H);
502impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F, G, H, I);
503impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F, G, H, I, J);
504
505/// Start a new slot worker.
506///
507/// Every time a new slot is triggered, `worker.on_slot` is called and the future it returns is
508/// polled until completion, unless we are major syncing.
509pub async fn start_slot_worker<B, C, W, SO, CIDP, Proof>(
510	slot_duration: SlotDuration,
511	client: C,
512	mut worker: W,
513	sync_oracle: SO,
514	create_inherent_data_providers: CIDP,
515) where
516	B: BlockT,
517	C: SelectChain<B>,
518	W: SlotWorker<B, Proof>,
519	SO: SyncOracle + Send,
520	CIDP: CreateInherentDataProviders<B, ()> + Send + 'static,
521	CIDP::InherentDataProviders: InherentDataProviderExt + Send,
522{
523	let mut slots = Slots::new(
524		slot_duration.as_duration(),
525		create_inherent_data_providers,
526		client,
527		sync_oracle,
528	);
529
530	loop {
531		let slot_info = slots.next_slot().await;
532		let _ = worker.on_slot(slot_info).await;
533	}
534}
535
536/// A header which has been checked
537pub enum CheckedHeader<H, S> {
538	/// A header which has slot in the future. this is the full header (not stripped)
539	/// and the slot in which it should be processed.
540	Deferred(H, Slot),
541	/// A header which is fully checked, including signature. This is the pre-header
542	/// accompanied by the seal components.
543	///
544	/// Includes the digest item that encoded the seal.
545	Checked(H, S),
546}
547
548/// A unit type wrapper to express the proportion of a slot.
549pub struct SlotProportion(f32);
550
551impl SlotProportion {
552	/// Create a new proportion.
553	///
554	/// The given value `inner` should be in the range `[0,1]`. If the value is not in the required
555	/// range, it is clamped into the range.
556	pub fn new(inner: f32) -> Self {
557		Self(inner.clamp(0.0, 1.0))
558	}
559
560	/// Returns the inner that is guaranteed to be in the range `[0,1]`.
561	pub fn get(&self) -> f32 {
562		self.0
563	}
564}
565
566/// The strategy used to calculate the slot lenience used to increase the block proposal time when
567/// slots have been skipped with no blocks authored.
568pub enum SlotLenienceType {
569	/// Increase the lenience linearly with the number of skipped slots.
570	Linear,
571	/// Increase the lenience exponentially with the number of skipped slots.
572	Exponential,
573}
574
575impl SlotLenienceType {
576	fn as_str(&self) -> &'static str {
577		match self {
578			SlotLenienceType::Linear => "linear",
579			SlotLenienceType::Exponential => "exponential",
580		}
581	}
582}
583
584/// Calculate the remaining duration for block proposal taking into account whether any slots have
585/// been skipped and applying the given lenience strategy. If `max_block_proposal_slot_portion` is
586/// not none this method guarantees that the returned duration must be lower or equal to
587/// `slot_info.duration * max_block_proposal_slot_portion`.
588pub fn proposing_remaining_duration<Block: BlockT>(
589	parent_slot: Option<Slot>,
590	slot_info: &SlotInfo<Block>,
591	block_proposal_slot_portion: &SlotProportion,
592	max_block_proposal_slot_portion: Option<&SlotProportion>,
593	slot_lenience_type: SlotLenienceType,
594	log_target: &str,
595) -> Duration {
596	use pezsp_runtime::traits::Zero;
597
598	let proposing_duration = slot_info.duration.mul_f32(block_proposal_slot_portion.get());
599
600	let slot_remaining = slot_info
601		.ends_at
602		.checked_duration_since(std::time::Instant::now())
603		.unwrap_or_default();
604
605	let proposing_duration = std::cmp::min(slot_remaining, proposing_duration);
606
607	// If parent is genesis block, we don't require any lenience factor.
608	if slot_info.chain_head.number().is_zero() {
609		return proposing_duration;
610	}
611
612	let parent_slot = match parent_slot {
613		Some(parent_slot) => parent_slot,
614		None => return proposing_duration,
615	};
616
617	let slot_lenience = match slot_lenience_type {
618		SlotLenienceType::Exponential => slot_lenience_exponential(parent_slot, slot_info),
619		SlotLenienceType::Linear => slot_lenience_linear(parent_slot, slot_info),
620	};
621
622	if let Some(slot_lenience) = slot_lenience {
623		let lenient_proposing_duration =
624			proposing_duration + slot_lenience.mul_f32(block_proposal_slot_portion.get());
625
626		// if we defined a maximum portion of the slot for proposal then we must make sure the
627		// lenience doesn't go over it
628		let lenient_proposing_duration =
629			if let Some(max_block_proposal_slot_portion) = max_block_proposal_slot_portion {
630				std::cmp::min(
631					lenient_proposing_duration,
632					slot_info.duration.mul_f32(max_block_proposal_slot_portion.get()),
633				)
634			} else {
635				lenient_proposing_duration
636			};
637
638		debug!(
639			target: log_target,
640			"No block for {} slots. Applying {} lenience, total proposing duration: {}ms",
641			slot_info.slot.saturating_sub(parent_slot + 1),
642			slot_lenience_type.as_str(),
643			lenient_proposing_duration.as_millis(),
644		);
645
646		lenient_proposing_duration
647	} else {
648		proposing_duration
649	}
650}
651
652/// Calculate a slot duration lenience based on the number of missed slots from current
653/// to parent. If the number of skipped slots is greater than 0 this method will apply
654/// an exponential backoff of at most `2^7 * slot_duration`, if no slots were skipped
655/// this method will return `None.`
656pub fn slot_lenience_exponential<Block: BlockT>(
657	parent_slot: Slot,
658	slot_info: &SlotInfo<Block>,
659) -> Option<Duration> {
660	// never give more than 2^this times the lenience.
661	const BACKOFF_CAP: u64 = 7;
662
663	// how many slots it takes before we double the lenience.
664	const BACKOFF_STEP: u64 = 2;
665
666	// we allow a lenience of the number of slots since the head of the
667	// chain was produced, minus 1 (since there is always a difference of at least 1)
668	//
669	// exponential back-off.
670	// in normal cases we only attempt to issue blocks up to the end of the slot.
671	// when the chain has been stalled for a few slots, we give more lenience.
672	let skipped_slots = *slot_info.slot.saturating_sub(parent_slot + 1);
673
674	if skipped_slots == 0 {
675		None
676	} else {
677		let slot_lenience = skipped_slots / BACKOFF_STEP;
678		let slot_lenience = std::cmp::min(slot_lenience, BACKOFF_CAP);
679		let slot_lenience = 1 << slot_lenience;
680		Some(slot_lenience * slot_info.duration)
681	}
682}
683
684/// Calculate a slot duration lenience based on the number of missed slots from current
685/// to parent. If the number of skipped slots is greater than 0 this method will apply
686/// a linear backoff of at most `20 * slot_duration`, if no slots were skipped
687/// this method will return `None.`
688pub fn slot_lenience_linear<Block: BlockT>(
689	parent_slot: Slot,
690	slot_info: &SlotInfo<Block>,
691) -> Option<Duration> {
692	// never give more than 20 times more lenience.
693	const BACKOFF_CAP: u64 = 20;
694
695	// we allow a lenience of the number of slots since the head of the
696	// chain was produced, minus 1 (since there is always a difference of at least 1)
697	//
698	// linear back-off.
699	// in normal cases we only attempt to issue blocks up to the end of the slot.
700	// when the chain has been stalled for a few slots, we give more lenience.
701	let skipped_slots = *slot_info.slot.saturating_sub(parent_slot + 1);
702
703	if skipped_slots == 0 {
704		None
705	} else {
706		let slot_lenience = std::cmp::min(skipped_slots, BACKOFF_CAP);
707		// We cap `slot_lenience` to `20`, so it should always fit into an `u32`.
708		Some(slot_info.duration * (slot_lenience as u32))
709	}
710}
711
712/// Trait for providing the strategy for when to backoff block authoring.
713pub trait BackoffAuthoringBlocksStrategy<N> {
714	/// Returns true if we should backoff authoring new blocks.
715	fn should_backoff(
716		&self,
717		chain_head_number: N,
718		chain_head_slot: Slot,
719		finalized_number: N,
720		slow_now: Slot,
721		logging_target: &str,
722	) -> bool;
723}
724
725/// A simple default strategy for how to decide backing off authoring blocks if the number of
726/// unfinalized blocks grows too large.
727#[derive(Clone)]
728pub struct BackoffAuthoringOnFinalizedHeadLagging<N> {
729	/// The max interval to backoff when authoring blocks, regardless of delay in finality.
730	pub max_interval: N,
731	/// The number of unfinalized blocks allowed before starting to consider to backoff authoring
732	/// blocks. Note that depending on the value for `authoring_bias`, there might still be an
733	/// additional wait until block authorship starts getting declined.
734	pub unfinalized_slack: N,
735	/// Scales the backoff rate. A higher value effectively means we backoff slower, taking longer
736	/// time to reach the maximum backoff as the unfinalized head of chain grows.
737	pub authoring_bias: N,
738}
739
740/// These parameters is supposed to be some form of sensible defaults.
741impl<N: BaseArithmetic> Default for BackoffAuthoringOnFinalizedHeadLagging<N> {
742	fn default() -> Self {
743		Self {
744			// Never wait more than 100 slots before authoring blocks, regardless of delay in
745			// finality.
746			max_interval: 100.into(),
747			// Start to consider backing off block authorship once we have 50 or more unfinalized
748			// blocks at the head of the chain.
749			unfinalized_slack: 50.into(),
750			// A reasonable default for the authoring bias, or reciprocal interval scaling, is 2.
751			// Effectively meaning that consider the unfinalized head suffix length to grow half as
752			// fast as in actuality.
753			authoring_bias: 2.into(),
754		}
755	}
756}
757
758impl<N> BackoffAuthoringBlocksStrategy<N> for BackoffAuthoringOnFinalizedHeadLagging<N>
759where
760	N: BaseArithmetic + Copy,
761{
762	fn should_backoff(
763		&self,
764		chain_head_number: N,
765		chain_head_slot: Slot,
766		finalized_number: N,
767		slot_now: Slot,
768		logging_target: &str,
769	) -> bool {
770		// This should not happen, but we want to keep the previous behaviour if it does.
771		if slot_now <= chain_head_slot {
772			return false;
773		}
774
775		// There can be race between getting the finalized number and getting the best number.
776		// So, better be safe than sorry.
777		let unfinalized_block_length = chain_head_number.saturating_sub(finalized_number);
778		let interval =
779			unfinalized_block_length.saturating_sub(self.unfinalized_slack) / self.authoring_bias;
780		let interval = interval.min(self.max_interval);
781
782		// We're doing arithmetic between block and slot numbers.
783		let interval: u64 = interval.unique_saturated_into();
784
785		// If interval is nonzero we backoff if the current slot isn't far enough ahead of the chain
786		// head.
787		if *slot_now <= *chain_head_slot + interval {
788			info!(
789				target: logging_target,
790				"Backing off claiming new slot for block authorship: finality is lagging.",
791			);
792			true
793		} else {
794			false
795		}
796	}
797}
798
799impl<N> BackoffAuthoringBlocksStrategy<N> for () {
800	fn should_backoff(
801		&self,
802		_chain_head_number: N,
803		_chain_head_slot: Slot,
804		_finalized_number: N,
805		_slot_now: Slot,
806		_logging_target: &str,
807	) -> bool {
808		false
809	}
810}
811
812#[cfg(test)]
813mod test {
814	use super::*;
815	use bizinikiwi_test_runtime_client::runtime::{Block, Header};
816	use pezsp_runtime::traits::NumberFor;
817	use std::time::{Duration, Instant};
818
819	const SLOT_DURATION: Duration = Duration::from_millis(6000);
820
821	fn slot(slot: u64) -> super::slots::SlotInfo<Block> {
822		super::slots::SlotInfo {
823			slot: slot.into(),
824			duration: SLOT_DURATION,
825			create_inherent_data: Box::new(()),
826			ends_at: Instant::now() + SLOT_DURATION,
827			chain_head: Header::new(
828				1,
829				Default::default(),
830				Default::default(),
831				Default::default(),
832				Default::default(),
833			),
834			block_size_limit: None,
835		}
836	}
837
838	#[test]
839	fn linear_slot_lenience() {
840		// if no slots are skipped there should be no lenience
841		assert_eq!(super::slot_lenience_linear(1u64.into(), &slot(2)), None);
842
843		// otherwise the lenience is incremented linearly with
844		// the number of skipped slots.
845		for n in 3..=22 {
846			assert_eq!(
847				super::slot_lenience_linear(1u64.into(), &slot(n)),
848				Some(SLOT_DURATION * (n - 2) as u32),
849			);
850		}
851
852		// but we cap it to a maximum of 20 slots
853		assert_eq!(super::slot_lenience_linear(1u64.into(), &slot(23)), Some(SLOT_DURATION * 20));
854	}
855
856	#[test]
857	fn exponential_slot_lenience() {
858		// if no slots are skipped there should be no lenience
859		assert_eq!(super::slot_lenience_exponential(1u64.into(), &slot(2)), None);
860
861		// otherwise the lenience is incremented exponentially every two slots
862		for n in 3..=17 {
863			assert_eq!(
864				super::slot_lenience_exponential(1u64.into(), &slot(n)),
865				Some(SLOT_DURATION * 2u32.pow((n / 2 - 1) as u32)),
866			);
867		}
868
869		// but we cap it to a maximum of 14 slots
870		assert_eq!(
871			super::slot_lenience_exponential(1u64.into(), &slot(18)),
872			Some(SLOT_DURATION * 2u32.pow(7)),
873		);
874
875		assert_eq!(
876			super::slot_lenience_exponential(1u64.into(), &slot(19)),
877			Some(SLOT_DURATION * 2u32.pow(7)),
878		);
879	}
880
881	#[test]
882	fn proposing_remaining_duration_should_apply_lenience_based_on_proposal_slot_proportion() {
883		assert_eq!(
884			proposing_remaining_duration(
885				Some(0.into()),
886				&slot(2),
887				&SlotProportion(0.25),
888				None,
889				SlotLenienceType::Linear,
890				"test",
891			),
892			SLOT_DURATION.mul_f32(0.25 * 2.0),
893		);
894	}
895
896	#[test]
897	fn proposing_remaining_duration_should_never_exceed_max_proposal_slot_proportion() {
898		assert_eq!(
899			proposing_remaining_duration(
900				Some(0.into()),
901				&slot(100),
902				&SlotProportion(0.25),
903				Some(SlotProportion(0.9)).as_ref(),
904				SlotLenienceType::Exponential,
905				"test",
906			),
907			SLOT_DURATION.mul_f32(0.9),
908		);
909	}
910
911	#[derive(PartialEq, Debug)]
912	struct HeadState {
913		head_number: NumberFor<Block>,
914		head_slot: u64,
915		slot_now: NumberFor<Block>,
916	}
917
918	impl HeadState {
919		fn author_block(&mut self) {
920			// Add a block to the head, and set latest slot to the current
921			self.head_number += 1;
922			self.head_slot = self.slot_now;
923			// Advance slot to next
924			self.slot_now += 1;
925		}
926
927		fn dont_author_block(&mut self) {
928			self.slot_now += 1;
929		}
930	}
931
932	#[test]
933	fn should_never_backoff_when_head_not_advancing() {
934		let strategy = BackoffAuthoringOnFinalizedHeadLagging::<NumberFor<Block>> {
935			max_interval: 100,
936			unfinalized_slack: 5,
937			authoring_bias: 2,
938		};
939
940		let head_number = 1;
941		let head_slot = 1;
942		let finalized_number = 1;
943		let slot_now = 2;
944
945		let should_backoff: Vec<bool> = (slot_now..1000)
946			.map(|s| {
947				strategy.should_backoff(
948					head_number,
949					head_slot.into(),
950					finalized_number,
951					s.into(),
952					"slots",
953				)
954			})
955			.collect();
956
957		// Should always be false, since the head isn't advancing
958		let expected: Vec<bool> = (slot_now..1000).map(|_| false).collect();
959		assert_eq!(should_backoff, expected);
960	}
961
962	#[test]
963	fn should_stop_authoring_if_blocks_are_still_produced_when_finality_stalled() {
964		let strategy = BackoffAuthoringOnFinalizedHeadLagging::<NumberFor<Block>> {
965			max_interval: 100,
966			unfinalized_slack: 5,
967			authoring_bias: 2,
968		};
969
970		let mut head_number = 1;
971		let mut head_slot = 1;
972		let finalized_number = 1;
973		let slot_now = 2;
974
975		let should_backoff: Vec<bool> = (slot_now..300)
976			.map(move |s| {
977				let b = strategy.should_backoff(
978					head_number,
979					head_slot.into(),
980					finalized_number,
981					s.into(),
982					"slots",
983				);
984				// Chain is still advancing (by someone else)
985				head_number += 1;
986				head_slot = s;
987				b
988			})
989			.collect();
990
991		// Should always be true after a short while, since the chain is advancing but finality is
992		// stalled
993		let expected: Vec<bool> = (slot_now..300).map(|s| s > 8).collect();
994		assert_eq!(should_backoff, expected);
995	}
996
997	#[test]
998	fn should_never_backoff_if_max_interval_is_reached() {
999		let strategy = BackoffAuthoringOnFinalizedHeadLagging::<NumberFor<Block>> {
1000			max_interval: 100,
1001			unfinalized_slack: 5,
1002			authoring_bias: 2,
1003		};
1004
1005		// The limit `max_interval` is used when the unfinalized chain grows to
1006		// 	`max_interval * authoring_bias + unfinalized_slack`,
1007		// which for the above parameters becomes
1008		// 	100 * 2 + 5 = 205.
1009		// Hence we trigger this with head_number > finalized_number + 205.
1010		let head_number = 207;
1011		let finalized_number = 1;
1012
1013		// The limit is then used once the current slot is `max_interval` ahead of slot of the head.
1014		let head_slot = 1;
1015		let slot_now = 2;
1016		let max_interval = strategy.max_interval;
1017
1018		let should_backoff: Vec<bool> = (slot_now..200)
1019			.map(|s| {
1020				strategy.should_backoff(
1021					head_number,
1022					head_slot.into(),
1023					finalized_number,
1024					s.into(),
1025					"slots",
1026				)
1027			})
1028			.collect();
1029
1030		// Should backoff (true) until we are `max_interval` number of slots ahead of the chain
1031		// head slot, then we never backoff (false).
1032		let expected: Vec<bool> = (slot_now..200).map(|s| s <= max_interval + head_slot).collect();
1033		assert_eq!(should_backoff, expected);
1034	}
1035
1036	#[test]
1037	fn should_backoff_authoring_when_finality_stalled() {
1038		let param = BackoffAuthoringOnFinalizedHeadLagging {
1039			max_interval: 100,
1040			unfinalized_slack: 5,
1041			authoring_bias: 2,
1042		};
1043
1044		let finalized_number = 2;
1045		let mut head_state = HeadState { head_number: 4, head_slot: 10, slot_now: 11 };
1046
1047		let should_backoff = |head_state: &HeadState| -> bool {
1048			<dyn BackoffAuthoringBlocksStrategy<NumberFor<Block>>>::should_backoff(
1049				&param,
1050				head_state.head_number,
1051				head_state.head_slot.into(),
1052				finalized_number,
1053				head_state.slot_now.into(),
1054				"slots",
1055			)
1056		};
1057
1058		let backoff: Vec<bool> = (head_state.slot_now..200)
1059			.map(|_| {
1060				if should_backoff(&head_state) {
1061					head_state.dont_author_block();
1062					true
1063				} else {
1064					head_state.author_block();
1065					false
1066				}
1067			})
1068			.collect();
1069
1070		// Gradually start to backoff more and more frequently
1071		let expected = [
1072			false, false, false, false, false, // no effect
1073			true, false, true, false, // 1:1
1074			true, true, false, true, true, false, // 2:1
1075			true, true, true, false, true, true, true, false, // 3:1
1076			true, true, true, true, false, true, true, true, true, false, // 4:1
1077			true, true, true, true, true, false, true, true, true, true, true, false, // 5:1
1078			true, true, true, true, true, true, false, true, true, true, true, true, true,
1079			false, // 6:1
1080			true, true, true, true, true, true, true, false, true, true, true, true, true, true,
1081			true, false, // 7:1
1082			true, true, true, true, true, true, true, true, false, true, true, true, true, true,
1083			true, true, true, false, // 8:1
1084			true, true, true, true, true, true, true, true, true, false, true, true, true, true,
1085			true, true, true, true, true, false, // 9:1
1086			true, true, true, true, true, true, true, true, true, true, false, true, true, true,
1087			true, true, true, true, true, true, true, false, // 10:1
1088			true, true, true, true, true, true, true, true, true, true, true, false, true, true,
1089			true, true, true, true, true, true, true, true, true, false, // 11:1
1090			true, true, true, true, true, true, true, true, true, true, true, true, false, true,
1091			true, true, true, true, true, true, true, true, true, true, true, false, // 12:1
1092			true, true, true, true,
1093		];
1094
1095		assert_eq!(backoff.as_slice(), &expected[..]);
1096	}
1097
1098	#[test]
1099	fn should_never_wait_more_than_max_interval() {
1100		let param = BackoffAuthoringOnFinalizedHeadLagging {
1101			max_interval: 100,
1102			unfinalized_slack: 5,
1103			authoring_bias: 2,
1104		};
1105
1106		let finalized_number = 2;
1107		let starting_slot = 11;
1108		let mut head_state = HeadState { head_number: 4, head_slot: 10, slot_now: starting_slot };
1109
1110		let should_backoff = |head_state: &HeadState| -> bool {
1111			<dyn BackoffAuthoringBlocksStrategy<NumberFor<Block>>>::should_backoff(
1112				&param,
1113				head_state.head_number,
1114				head_state.head_slot.into(),
1115				finalized_number,
1116				head_state.slot_now.into(),
1117				"slots",
1118			)
1119		};
1120
1121		let backoff: Vec<bool> = (head_state.slot_now..40000)
1122			.map(|_| {
1123				if should_backoff(&head_state) {
1124					head_state.dont_author_block();
1125					true
1126				} else {
1127					head_state.author_block();
1128					false
1129				}
1130			})
1131			.collect();
1132
1133		let slots_claimed: Vec<usize> = backoff
1134			.iter()
1135			.enumerate()
1136			.filter(|&(_i, x)| x == &false)
1137			.map(|(i, _x)| i + starting_slot as usize)
1138			.collect();
1139
1140		let last_slot = backoff.len() + starting_slot as usize;
1141		let mut last_two_claimed = slots_claimed.iter().rev().take(2);
1142
1143		// Check that we claimed all the way to the end. Check two slots for when we have an uneven
1144		// number of slots_claimed.
1145		let expected_distance = param.max_interval as usize + 1;
1146		assert_eq!(last_slot - last_two_claimed.next().unwrap(), 92);
1147		assert_eq!(last_slot - last_two_claimed.next().unwrap(), 92 + expected_distance);
1148
1149		let intervals: Vec<_> = slots_claimed.windows(2).map(|x| x[1] - x[0]).collect();
1150
1151		// The key thing is that the distance between claimed slots is capped to `max_interval + 1`
1152		// assert_eq!(max_observed_interval, Some(&expected_distance));
1153		assert_eq!(intervals.iter().max(), Some(&expected_distance));
1154
1155		// But lets assert all distances, which we expect to grow linearly until `max_interval + 1`
1156		let expected_intervals: Vec<_> =
1157			(0..497).map(|i| (i / 2).clamp(1, expected_distance)).collect();
1158
1159		assert_eq!(intervals, expected_intervals);
1160	}
1161
1162	fn run_until_max_interval(param: BackoffAuthoringOnFinalizedHeadLagging<u64>) -> (u64, u64) {
1163		let finalized_number = 0;
1164		let mut head_state = HeadState { head_number: 0, head_slot: 0, slot_now: 1 };
1165
1166		let should_backoff = |head_state: &HeadState| -> bool {
1167			<dyn BackoffAuthoringBlocksStrategy<NumberFor<Block>>>::should_backoff(
1168				&param,
1169				head_state.head_number,
1170				head_state.head_slot.into(),
1171				finalized_number,
1172				head_state.slot_now.into(),
1173				"slots",
1174			)
1175		};
1176
1177		// Number of blocks until we reach the max interval
1178		let block_for_max_interval =
1179			param.max_interval * param.authoring_bias + param.unfinalized_slack;
1180
1181		while head_state.head_number < block_for_max_interval {
1182			if should_backoff(&head_state) {
1183				head_state.dont_author_block();
1184			} else {
1185				head_state.author_block();
1186			}
1187		}
1188
1189		let slot_time = 6;
1190		let time_to_reach_limit = slot_time * head_state.slot_now;
1191		(block_for_max_interval, time_to_reach_limit)
1192	}
1193
1194	// Denoting
1195	// 	C: unfinalized_slack
1196	// 	M: authoring_bias
1197	// 	X: max_interval
1198	// then the number of slots to reach the max interval can be computed from
1199	// 	(start_slot + C) + M * sum(n, 1, X)
1200	// or
1201	// 	(start_slot + C) + M * X*(X+1)/2
1202	fn expected_time_to_reach_max_interval(
1203		param: &BackoffAuthoringOnFinalizedHeadLagging<u64>,
1204	) -> (u64, u64) {
1205		let c = param.unfinalized_slack;
1206		let m = param.authoring_bias;
1207		let x = param.max_interval;
1208		let slot_time = 6;
1209
1210		let block_for_max_interval = x * m + c;
1211
1212		// The 1 is because we start at slot_now = 1.
1213		let expected_number_of_slots = (1 + c) + m * x * (x + 1) / 2;
1214		let time_to_reach = expected_number_of_slots * slot_time;
1215
1216		(block_for_max_interval, time_to_reach)
1217	}
1218
1219	#[test]
1220	fn time_to_reach_upper_bound_for_smaller_slack() {
1221		let param = BackoffAuthoringOnFinalizedHeadLagging {
1222			max_interval: 100,
1223			unfinalized_slack: 5,
1224			authoring_bias: 2,
1225		};
1226		let expected = expected_time_to_reach_max_interval(&param);
1227		let (block_for_max_interval, time_to_reach_limit) = run_until_max_interval(param);
1228		assert_eq!((block_for_max_interval, time_to_reach_limit), expected);
1229		// Note: 16 hours is 57600 sec
1230		assert_eq!((block_for_max_interval, time_to_reach_limit), (205, 60636));
1231	}
1232
1233	#[test]
1234	fn time_to_reach_upper_bound_for_larger_slack() {
1235		let param = BackoffAuthoringOnFinalizedHeadLagging {
1236			max_interval: 100,
1237			unfinalized_slack: 50,
1238			authoring_bias: 2,
1239		};
1240		let expected = expected_time_to_reach_max_interval(&param);
1241		let (block_for_max_interval, time_to_reach_limit) = run_until_max_interval(param);
1242		assert_eq!((block_for_max_interval, time_to_reach_limit), expected);
1243		assert_eq!((block_for_max_interval, time_to_reach_limit), (250, 60906));
1244	}
1245}