1#![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
55pub type StorageChanges<Block> = pezsp_state_machine::StorageChanges<HashingFor<Block>>;
59
60#[derive(Debug, Clone)]
62pub struct SlotResult<Block: BlockT, Proof> {
63 pub block: Block,
65 pub storage_proof: Proof,
67}
68
69#[async_trait::async_trait]
74pub trait SlotWorker<B: BlockT, Proof> {
75 async fn on_slot(&mut self, slot_info: SlotInfo<B>) -> Option<SlotResult<B, Proof>>;
80}
81
82#[async_trait::async_trait]
86pub trait SimpleSlotWorker<B: BlockT> {
87 type BlockImport: BlockImport<B> + Send + 'static;
89
90 type SyncOracle: SyncOracle;
92
93 type JustificationSyncLink: JustificationSyncLink<B>;
96
97 type CreateProposer: Future<Output = Result<Self::Proposer, pezsp_consensus::Error>>
99 + Send
100 + Unpin
101 + 'static;
102
103 type Proposer: Proposer<B> + Send;
105
106 type Claim: Send + Sync + 'static;
108
109 type AuxData: Send + Sync + 'static;
111
112 fn logging_target(&self) -> &'static str;
114
115 fn block_import(&mut self) -> &mut Self::BlockImport;
117
118 fn aux_data(
120 &self,
121 header: &B::Header,
122 slot: Slot,
123 ) -> Result<Self::AuxData, pezsp_consensus::Error>;
124
125 fn authorities_len(&self, aux_data: &Self::AuxData) -> Option<usize>;
128
129 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 fn notify_slot(&self, _header: &B::Header, _slot: Slot, _aux_data: &Self::AuxData) {}
140
141 fn pre_digest_data(&self, slot: Slot, claim: &Self::Claim) -> Vec<pezsp_runtime::DigestItem>;
143
144 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 fn force_authoring(&self) -> bool;
157
158 fn should_backoff(&self, _slot: Slot, _chain_head: &B::Header) -> bool {
165 false
166 }
167
168 fn sync_oracle(&mut self) -> &mut Self::SyncOracle;
170
171 fn justification_sync_link(&mut self) -> &mut Self::JustificationSyncLink;
173
174 fn proposer(&mut self, block: &B::Header) -> Self::CreateProposer;
176
177 fn telemetry(&self) -> Option<TelemetryHandle>;
179
180 fn proposing_remaining_duration(&self, slot_info: &SlotInfo<B>) -> Duration;
182
183 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 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 #[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 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 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
453pub 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
472pub trait InherentDataProviderExt {
474 fn slot(&self) -> Slot;
477}
478
479macro_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
505pub 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
536pub enum CheckedHeader<H, S> {
538 Deferred(H, Slot),
541 Checked(H, S),
546}
547
548pub struct SlotProportion(f32);
550
551impl SlotProportion {
552 pub fn new(inner: f32) -> Self {
557 Self(inner.clamp(0.0, 1.0))
558 }
559
560 pub fn get(&self) -> f32 {
562 self.0
563 }
564}
565
566pub enum SlotLenienceType {
569 Linear,
571 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
584pub 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 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 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
652pub fn slot_lenience_exponential<Block: BlockT>(
657 parent_slot: Slot,
658 slot_info: &SlotInfo<Block>,
659) -> Option<Duration> {
660 const BACKOFF_CAP: u64 = 7;
662
663 const BACKOFF_STEP: u64 = 2;
665
666 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
684pub fn slot_lenience_linear<Block: BlockT>(
689 parent_slot: Slot,
690 slot_info: &SlotInfo<Block>,
691) -> Option<Duration> {
692 const BACKOFF_CAP: u64 = 20;
694
695 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 Some(slot_info.duration * (slot_lenience as u32))
709 }
710}
711
712pub trait BackoffAuthoringBlocksStrategy<N> {
714 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#[derive(Clone)]
728pub struct BackoffAuthoringOnFinalizedHeadLagging<N> {
729 pub max_interval: N,
731 pub unfinalized_slack: N,
735 pub authoring_bias: N,
738}
739
740impl<N: BaseArithmetic> Default for BackoffAuthoringOnFinalizedHeadLagging<N> {
742 fn default() -> Self {
743 Self {
744 max_interval: 100.into(),
747 unfinalized_slack: 50.into(),
750 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 if slot_now <= chain_head_slot {
772 return false;
773 }
774
775 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 let interval: u64 = interval.unique_saturated_into();
784
785 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 assert_eq!(super::slot_lenience_linear(1u64.into(), &slot(2)), None);
842
843 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 assert_eq!(super::slot_lenience_linear(1u64.into(), &slot(23)), Some(SLOT_DURATION * 20));
854 }
855
856 #[test]
857 fn exponential_slot_lenience() {
858 assert_eq!(super::slot_lenience_exponential(1u64.into(), &slot(2)), None);
860
861 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 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 self.head_number += 1;
922 self.head_slot = self.slot_now;
923 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 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 head_number += 1;
986 head_slot = s;
987 b
988 })
989 .collect();
990
991 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 let head_number = 207;
1011 let finalized_number = 1;
1012
1013 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 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 ¶m,
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 let expected = [
1072 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,
1079 false, true, true, true, true, true, true, true, false, true, true, true, true, true, true,
1081 true, false, true, true, true, true, true, true, true, true, false, true, true, true, true, true,
1083 true, true, true, false, true, true, true, true, true, true, true, true, true, false, true, true, true, true,
1085 true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true, false, true, true, true,
1087 true, true, true, true, true, true, true, false, 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, 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, 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 ¶m,
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 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 assert_eq!(intervals.iter().max(), Some(&expected_distance));
1154
1155 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 ¶m,
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 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 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 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(¶m);
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 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(¶m);
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}