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 sc_consensus::{BlockImport, JustificationSyncLink};
39use sc_telemetry::{telemetry, TelemetryHandle, CONSENSUS_DEBUG, CONSENSUS_INFO, CONSENSUS_WARN};
40use sp_arithmetic::traits::BaseArithmetic;
41use sp_consensus::{Proposal, ProposeArgs, Proposer, SelectChain, SyncOracle};
42use sp_consensus_slots::{Slot, SlotDuration};
43use sp_inherents::CreateInherentDataProviders;
44use sp_runtime::traits::{Block as BlockT, HashingFor, Header as HeaderT};
45use std::{
46 ops::Deref,
47 time::{Duration, Instant},
48};
49
50const LOG_TARGET: &str = "slots";
51
52pub type StorageChanges<Block> = sp_state_machine::StorageChanges<HashingFor<Block>>;
56
57#[async_trait::async_trait]
62pub trait SlotWorker<B: BlockT> {
63 async fn on_slot(&mut self, slot_info: SlotInfo<B>) -> Option<B>;
69}
70
71#[async_trait::async_trait]
75pub trait SimpleSlotWorker<B: BlockT> {
76 type BlockImport: BlockImport<B> + Send + 'static;
78
79 type SyncOracle: SyncOracle;
81
82 type JustificationSyncLink: JustificationSyncLink<B>;
85
86 type CreateProposer: Future<Output = Result<Self::Proposer, sp_consensus::Error>>
88 + Send
89 + Unpin
90 + 'static;
91
92 type Proposer: Proposer<B> + Send;
94
95 type Claim: Send + Sync + 'static;
97
98 type AuxData: Send + Sync + 'static;
100
101 fn logging_target(&self) -> &'static str;
103
104 fn block_import(&mut self) -> &mut Self::BlockImport;
106
107 fn aux_data(
109 &self,
110 header: &B::Header,
111 slot: Slot,
112 ) -> Result<Self::AuxData, sp_consensus::Error>;
113
114 fn authorities_len(&self, aux_data: &Self::AuxData) -> Option<usize>;
117
118 async fn claim_slot(
120 &mut self,
121 header: &B::Header,
122 slot: Slot,
123 aux_data: &Self::AuxData,
124 ) -> Option<Self::Claim>;
125
126 fn notify_slot(&self, _header: &B::Header, _slot: Slot, _aux_data: &Self::AuxData) {}
129
130 fn pre_digest_data(&self, slot: Slot, claim: &Self::Claim) -> Vec<sp_runtime::DigestItem>;
132
133 async fn block_import_params(
135 &self,
136 header: B::Header,
137 header_hash: &B::Hash,
138 body: Vec<B::Extrinsic>,
139 storage_changes: StorageChanges<B>,
140 public: Self::Claim,
141 aux_data: Self::AuxData,
142 ) -> Result<sc_consensus::BlockImportParams<B>, sp_consensus::Error>;
143
144 fn force_authoring(&self) -> bool;
146
147 fn should_backoff(&self, _slot: Slot, _chain_head: &B::Header) -> bool {
154 false
155 }
156
157 fn sync_oracle(&mut self) -> &mut Self::SyncOracle;
159
160 fn justification_sync_link(&mut self) -> &mut Self::JustificationSyncLink;
162
163 fn proposer(&mut self, block: &B::Header) -> Self::CreateProposer;
165
166 fn telemetry(&self) -> Option<TelemetryHandle>;
168
169 fn proposing_remaining_duration(&self, slot_info: &SlotInfo<B>) -> Duration;
171
172 async fn propose(
174 &mut self,
175 proposer: Self::Proposer,
176 claim: &Self::Claim,
177 slot_info: SlotInfo<B>,
178 end_proposing_at: Instant,
179 ) -> Option<Proposal<B>> {
180 let slot = slot_info.slot;
181 let telemetry = self.telemetry();
182 let log_target = self.logging_target();
183
184 let inherent_data =
185 Self::create_inherent_data(&slot_info, &log_target, end_proposing_at).await?;
186
187 let proposing_remaining_duration =
188 end_proposing_at.saturating_duration_since(Instant::now());
189 let logs = self.pre_digest_data(slot, claim);
190
191 let propose_args = ProposeArgs {
195 inherent_data,
196 inherent_digests: sp_runtime::generic::Digest { logs },
197 max_duration: proposing_remaining_duration.mul_f32(0.98),
198 block_size_limit: slot_info.block_size_limit,
199 storage_proof_recorder: slot_info.storage_proof_recorder,
200 ..Default::default()
201 };
202
203 let proposing = proposer
204 .propose(propose_args)
205 .map_err(|e| sp_consensus::Error::ClientImport(e.to_string()));
206
207 let proposal = match futures::future::select(
208 proposing,
209 Delay::new(proposing_remaining_duration),
210 )
211 .await
212 {
213 Either::Left((Ok(p), _)) => p,
214 Either::Left((Err(err), _)) => {
215 warn!(target: log_target, "Proposing failed: {}", err);
216
217 return None;
218 },
219 Either::Right(_) => {
220 info!(
221 target: log_target,
222 "โ๏ธ Discarding proposal for slot {}; block production took too long", slot,
223 );
224 #[cfg(build_profile = "debug")]
226 info!(
227 target: log_target,
228 "๐ Recompile your node in `--release` mode to mitigate this problem.",
229 );
230 telemetry!(
231 telemetry;
232 CONSENSUS_INFO;
233 "slots.discarding_proposal_took_too_long";
234 "slot" => *slot,
235 );
236
237 return None;
238 },
239 };
240
241 Some(proposal)
242 }
243
244 async fn create_inherent_data(
246 slot_info: &SlotInfo<B>,
247 logging_target: &str,
248 end_proposing_at: Instant,
249 ) -> Option<sp_inherents::InherentData> {
250 let remaining_duration = end_proposing_at.saturating_duration_since(Instant::now());
251 let delay = Delay::new(remaining_duration);
252 let cid = slot_info.create_inherent_data.create_inherent_data();
253 let inherent_data = match futures::future::select(delay, cid).await {
254 Either::Right((Ok(data), _)) => data,
255 Either::Right((Err(err), _)) => {
256 warn!(
257 target: logging_target,
258 "Unable to create inherent data for block {:?}: {}",
259 slot_info.chain_head.hash(),
260 err,
261 );
262
263 return None;
264 },
265 Either::Left(_) => {
266 warn!(
267 target: logging_target,
268 "Creating inherent data took more time than we had left for slot {} for block {:?}.",
269 slot_info.slot,
270 slot_info.chain_head.hash(),
271 );
272
273 return None;
274 },
275 };
276
277 Some(inherent_data)
278 }
279
280 async fn on_slot(&mut self, slot_info: SlotInfo<B>) -> Option<B>
282 where
283 Self: Sync,
284 {
285 let slot = slot_info.slot;
286 let telemetry = self.telemetry();
287 let logging_target = self.logging_target();
288
289 let proposing_remaining_duration = self.proposing_remaining_duration(&slot_info);
290
291 let end_proposing_at = if proposing_remaining_duration == Duration::default() {
292 debug!(
293 target: logging_target,
294 "Skipping proposal slot {} since there's no time left to propose", slot,
295 );
296
297 return None;
298 } else {
299 Instant::now() + proposing_remaining_duration
300 };
301
302 let aux_data = match self.aux_data(&slot_info.chain_head, slot) {
303 Ok(aux_data) => aux_data,
304 Err(err) => {
305 warn!(
306 target: logging_target,
307 "Unable to fetch auxiliary data for block {:?}: {}",
308 slot_info.chain_head.hash(),
309 err,
310 );
311
312 telemetry!(
313 telemetry;
314 CONSENSUS_WARN;
315 "slots.unable_fetching_authorities";
316 "slot" => ?slot_info.chain_head.hash(),
317 "err" => ?err,
318 );
319
320 return None;
321 },
322 };
323
324 self.notify_slot(&slot_info.chain_head, slot, &aux_data);
325
326 let authorities_len = self.authorities_len(&aux_data);
327
328 if !self.force_authoring() &&
329 self.sync_oracle().is_offline() &&
330 authorities_len.map(|a| a > 1).unwrap_or(false)
331 {
332 debug!(target: logging_target, "Skipping proposal slot. Waiting for the network.");
333 telemetry!(
334 telemetry;
335 CONSENSUS_DEBUG;
336 "slots.skipping_proposal_slot";
337 "authorities_len" => authorities_len,
338 );
339
340 return None;
341 }
342
343 let claim = self.claim_slot(&slot_info.chain_head, slot, &aux_data).await?;
344
345 if self.should_backoff(slot, &slot_info.chain_head) {
346 return None;
347 }
348
349 debug!(target: logging_target, "Starting authorship at slot: {slot}");
350
351 telemetry!(telemetry; CONSENSUS_DEBUG; "slots.starting_authorship"; "slot_num" => slot);
352
353 let proposer = match self.proposer(&slot_info.chain_head).await {
354 Ok(p) => p,
355 Err(err) => {
356 warn!(target: logging_target, "Unable to author block in slot {slot:?}: {err}");
357
358 telemetry!(
359 telemetry;
360 CONSENSUS_WARN;
361 "slots.unable_authoring_block";
362 "slot" => *slot,
363 "err" => ?err
364 );
365
366 return None;
367 },
368 };
369
370 let proposal = self.propose(proposer, &claim, slot_info, end_proposing_at).await?;
371
372 let block = proposal.block;
373 let (header, body) = block.deconstruct();
374 let header_num = *header.number();
375 let header_hash = header.hash();
376 let parent_hash = *header.parent_hash();
377
378 let block_import_params = match self
379 .block_import_params(
380 header,
381 &header_hash,
382 body.clone(),
383 proposal.storage_changes,
384 claim,
385 aux_data,
386 )
387 .await
388 {
389 Ok(bi) => bi,
390 Err(err) => {
391 warn!(target: logging_target, "Failed to create block import params: {}", err);
392
393 return None;
394 },
395 };
396
397 info!(
398 target: logging_target,
399 "๐ Pre-sealed block for proposal at {}. Hash now {:?}, previously {:?}.",
400 header_num,
401 block_import_params.post_hash(),
402 header_hash,
403 );
404
405 telemetry!(
406 telemetry;
407 CONSENSUS_INFO;
408 "slots.pre_sealed_block";
409 "header_num" => ?header_num,
410 "hash_now" => ?block_import_params.post_hash(),
411 "hash_previously" => ?header_hash,
412 );
413
414 let header = block_import_params.post_header();
415 match self.block_import().import_block(block_import_params).await {
416 Ok(res) => {
417 res.handle_justification(
418 &header.hash(),
419 *header.number(),
420 self.justification_sync_link(),
421 );
422 },
423 Err(err) => {
424 warn!(
425 target: logging_target,
426 "Error with block built on {:?}: {}", parent_hash, err,
427 );
428
429 telemetry!(
430 telemetry;
431 CONSENSUS_WARN;
432 "slots.err_with_block_built_on";
433 "hash" => ?parent_hash,
434 "err" => ?err,
435 );
436 },
437 }
438
439 Some(B::new(header, body))
440 }
441}
442
443pub struct SimpleSlotWorkerToSlotWorker<T>(pub T);
449
450#[async_trait::async_trait]
451impl<T: SimpleSlotWorker<B> + Send + Sync, B: BlockT> SlotWorker<B>
452 for SimpleSlotWorkerToSlotWorker<T>
453{
454 async fn on_slot(&mut self, slot_info: SlotInfo<B>) -> Option<B> {
455 self.0.on_slot(slot_info).await
456 }
457}
458
459pub trait InherentDataProviderExt {
461 fn slot(&self) -> Slot;
463}
464
465macro_rules! impl_inherent_data_provider_ext_tuple {
467 ( S $(, $TN:ident)* $( , )?) => {
468 impl<S, $( $TN ),*> InherentDataProviderExt for (S, $($TN),*)
469 where
470 S: Deref<Target = Slot>,
471 {
472 fn slot(&self) -> Slot {
473 *self.0.deref()
474 }
475 }
476 }
477}
478
479impl_inherent_data_provider_ext_tuple!(S);
480impl_inherent_data_provider_ext_tuple!(S, A);
481impl_inherent_data_provider_ext_tuple!(S, A, B);
482impl_inherent_data_provider_ext_tuple!(S, A, B, C);
483impl_inherent_data_provider_ext_tuple!(S, A, B, C, D);
484impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E);
485impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F);
486impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F, G);
487impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F, G, H);
488impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F, G, H, I);
489impl_inherent_data_provider_ext_tuple!(S, A, B, C, D, E, F, G, H, I, J);
490
491pub async fn start_slot_worker<B, C, W, SO, CIDP>(
496 slot_duration: SlotDuration,
497 client: C,
498 mut worker: W,
499 sync_oracle: SO,
500 create_inherent_data_providers: CIDP,
501) where
502 B: BlockT,
503 C: SelectChain<B>,
504 W: SlotWorker<B>,
505 SO: SyncOracle + Send,
506 CIDP: CreateInherentDataProviders<B, ()> + Send + 'static,
507 CIDP::InherentDataProviders: InherentDataProviderExt + Send,
508{
509 let mut slots = Slots::new(
510 slot_duration.as_duration(),
511 create_inherent_data_providers,
512 client,
513 sync_oracle,
514 );
515
516 loop {
517 let slot_info = slots.next_slot().await;
518 let _ = worker.on_slot(slot_info).await;
519 }
520}
521
522pub enum CheckedHeader<H, S> {
524 Deferred(H, Slot),
527 Checked(H, S),
532}
533
534pub struct SlotProportion(f32);
536
537impl SlotProportion {
538 pub fn new(inner: f32) -> Self {
543 Self(inner.clamp(0.0, 1.0))
544 }
545
546 pub fn get(&self) -> f32 {
548 self.0
549 }
550}
551
552pub enum SlotLenienceType {
555 Linear,
557 Exponential,
559}
560
561impl SlotLenienceType {
562 fn as_str(&self) -> &'static str {
563 match self {
564 SlotLenienceType::Linear => "linear",
565 SlotLenienceType::Exponential => "exponential",
566 }
567 }
568}
569
570pub fn proposing_remaining_duration<Block: BlockT>(
575 parent_slot: Option<Slot>,
576 slot_info: &SlotInfo<Block>,
577 block_proposal_slot_portion: &SlotProportion,
578 max_block_proposal_slot_portion: Option<&SlotProportion>,
579 slot_lenience_type: SlotLenienceType,
580 log_target: &str,
581) -> Duration {
582 use sp_runtime::traits::Zero;
583
584 let proposing_duration = slot_info.duration.mul_f32(block_proposal_slot_portion.get());
585
586 let slot_remaining = slot_info
587 .ends_at
588 .checked_duration_since(std::time::Instant::now())
589 .unwrap_or_default();
590
591 let proposing_duration = std::cmp::min(slot_remaining, proposing_duration);
592
593 let max_proposing_duration =
594 max_block_proposal_slot_portion.map(|p| slot_info.duration.mul_f32(p.get()));
595
596 let proposing_duration = max_proposing_duration
599 .map_or(proposing_duration, |max| std::cmp::min(proposing_duration, max));
600
601 if slot_info.chain_head.number().is_zero() {
603 return proposing_duration;
604 }
605
606 let parent_slot = match parent_slot {
607 Some(parent_slot) => parent_slot,
608 None => return proposing_duration,
609 };
610
611 let slot_lenience = match slot_lenience_type {
612 SlotLenienceType::Exponential => slot_lenience_exponential(parent_slot, slot_info),
613 SlotLenienceType::Linear => slot_lenience_linear(parent_slot, slot_info),
614 };
615
616 if let Some(slot_lenience) = slot_lenience {
617 let lenient_proposing_duration =
618 proposing_duration + slot_lenience.mul_f32(block_proposal_slot_portion.get());
619
620 let lenient_proposing_duration = max_proposing_duration
623 .map_or(lenient_proposing_duration, |max| {
624 std::cmp::min(lenient_proposing_duration, max)
625 });
626
627 debug!(
628 target: log_target,
629 "No block for {} slots. Applying {} lenience, total proposing duration: {}ms",
630 slot_info.slot.saturating_sub(parent_slot + 1),
631 slot_lenience_type.as_str(),
632 lenient_proposing_duration.as_millis(),
633 );
634
635 lenient_proposing_duration
636 } else {
637 proposing_duration
638 }
639}
640
641pub fn slot_lenience_exponential<Block: BlockT>(
646 parent_slot: Slot,
647 slot_info: &SlotInfo<Block>,
648) -> Option<Duration> {
649 const BACKOFF_CAP: u64 = 7;
651
652 const BACKOFF_STEP: u64 = 2;
654
655 let skipped_slots = *slot_info.slot.saturating_sub(parent_slot + 1);
662
663 if skipped_slots == 0 {
664 None
665 } else {
666 let slot_lenience = skipped_slots / BACKOFF_STEP;
667 let slot_lenience = std::cmp::min(slot_lenience, BACKOFF_CAP);
668 let slot_lenience = 1 << slot_lenience;
669 Some(slot_lenience * slot_info.duration)
670 }
671}
672
673pub fn slot_lenience_linear<Block: BlockT>(
678 parent_slot: Slot,
679 slot_info: &SlotInfo<Block>,
680) -> Option<Duration> {
681 const BACKOFF_CAP: u64 = 20;
683
684 let skipped_slots = *slot_info.slot.saturating_sub(parent_slot + 1);
691
692 if skipped_slots == 0 {
693 None
694 } else {
695 let slot_lenience = std::cmp::min(skipped_slots, BACKOFF_CAP);
696 Some(slot_info.duration * (slot_lenience as u32))
698 }
699}
700
701pub trait BackoffAuthoringBlocksStrategy<N> {
703 fn should_backoff(
705 &self,
706 chain_head_number: N,
707 chain_head_slot: Slot,
708 finalized_number: N,
709 slow_now: Slot,
710 logging_target: &str,
711 ) -> bool;
712}
713
714#[derive(Clone)]
717pub struct BackoffAuthoringOnFinalizedHeadLagging<N> {
718 pub max_interval: N,
720 pub unfinalized_slack: N,
724 pub authoring_bias: N,
727}
728
729impl<N: BaseArithmetic> Default for BackoffAuthoringOnFinalizedHeadLagging<N> {
731 fn default() -> Self {
732 Self {
733 max_interval: 100.into(),
736 unfinalized_slack: 50.into(),
739 authoring_bias: 2.into(),
743 }
744 }
745}
746
747impl<N> BackoffAuthoringBlocksStrategy<N> for BackoffAuthoringOnFinalizedHeadLagging<N>
748where
749 N: BaseArithmetic + Copy,
750{
751 fn should_backoff(
752 &self,
753 chain_head_number: N,
754 chain_head_slot: Slot,
755 finalized_number: N,
756 slot_now: Slot,
757 logging_target: &str,
758 ) -> bool {
759 if slot_now <= chain_head_slot {
761 return false;
762 }
763
764 let unfinalized_block_length = chain_head_number.saturating_sub(finalized_number);
767 let interval =
768 unfinalized_block_length.saturating_sub(self.unfinalized_slack) / self.authoring_bias;
769 let interval = interval.min(self.max_interval);
770
771 let interval: u64 = interval.unique_saturated_into();
773
774 if *slot_now <= *chain_head_slot + interval {
777 info!(
778 target: logging_target,
779 "Backing off claiming new slot for block authorship: finality is lagging.",
780 );
781 true
782 } else {
783 false
784 }
785 }
786}
787
788impl<N> BackoffAuthoringBlocksStrategy<N> for () {
789 fn should_backoff(
790 &self,
791 _chain_head_number: N,
792 _chain_head_slot: Slot,
793 _finalized_number: N,
794 _slot_now: Slot,
795 _logging_target: &str,
796 ) -> bool {
797 false
798 }
799}
800
801#[cfg(test)]
802mod test {
803 use super::*;
804 use sp_runtime::traits::NumberFor;
805 use std::time::{Duration, Instant};
806 use substrate_test_runtime_client::runtime::{Block, Header};
807
808 const SLOT_DURATION: Duration = Duration::from_millis(6000);
809
810 fn slot(slot: u64) -> super::slots::SlotInfo<Block> {
811 slot_with_head_number(slot, 1)
812 }
813
814 fn slot_with_head_number(slot: u64, head_number: u64) -> super::slots::SlotInfo<Block> {
815 super::slots::SlotInfo {
816 slot: slot.into(),
817 duration: SLOT_DURATION,
818 create_inherent_data: Box::new(()),
819 ends_at: Instant::now() + SLOT_DURATION,
820 chain_head: Header::new(
821 head_number,
822 Default::default(),
823 Default::default(),
824 Default::default(),
825 Default::default(),
826 ),
827 block_size_limit: None,
828 storage_proof_recorder: None,
829 }
830 }
831
832 #[test]
833 fn linear_slot_lenience() {
834 assert_eq!(super::slot_lenience_linear(1u64.into(), &slot(2)), None);
836
837 for n in 3..=22 {
840 assert_eq!(
841 super::slot_lenience_linear(1u64.into(), &slot(n)),
842 Some(SLOT_DURATION * (n - 2) as u32),
843 );
844 }
845
846 assert_eq!(super::slot_lenience_linear(1u64.into(), &slot(23)), Some(SLOT_DURATION * 20));
848 }
849
850 #[test]
851 fn exponential_slot_lenience() {
852 assert_eq!(super::slot_lenience_exponential(1u64.into(), &slot(2)), None);
854
855 for n in 3..=17 {
857 assert_eq!(
858 super::slot_lenience_exponential(1u64.into(), &slot(n)),
859 Some(SLOT_DURATION * 2u32.pow((n / 2 - 1) as u32)),
860 );
861 }
862
863 assert_eq!(
865 super::slot_lenience_exponential(1u64.into(), &slot(18)),
866 Some(SLOT_DURATION * 2u32.pow(7)),
867 );
868
869 assert_eq!(
870 super::slot_lenience_exponential(1u64.into(), &slot(19)),
871 Some(SLOT_DURATION * 2u32.pow(7)),
872 );
873 }
874
875 #[test]
876 fn proposing_remaining_duration_should_apply_lenience_based_on_proposal_slot_proportion() {
877 assert_eq!(
878 proposing_remaining_duration(
879 Some(0.into()),
880 &slot(2),
881 &SlotProportion(0.25),
882 None,
883 SlotLenienceType::Linear,
884 "test",
885 ),
886 SLOT_DURATION.mul_f32(0.25 * 2.0),
887 );
888 }
889
890 #[test]
891 fn proposing_remaining_duration_should_never_exceed_max_proposal_slot_proportion() {
892 assert_eq!(
893 proposing_remaining_duration(
894 Some(0.into()),
895 &slot(100),
896 &SlotProportion(0.25),
897 Some(SlotProportion(0.9)).as_ref(),
898 SlotLenienceType::Exponential,
899 "test",
900 ),
901 SLOT_DURATION.mul_f32(0.9),
902 );
903 }
904
905 #[test]
906 fn proposing_remaining_duration_caps_every_path_at_max_proposal_slot_proportion() {
907 let block_portion = SlotProportion(0.5);
908 let max_portion = SlotProportion(0.25);
909 let expected = SLOT_DURATION.mul_f32(0.25);
910
911 assert_eq!(
913 proposing_remaining_duration(
914 Some(1.into()),
915 &slot(2),
916 &block_portion,
917 Some(&max_portion),
918 SlotLenienceType::Linear,
919 "test",
920 ),
921 expected,
922 );
923
924 assert_eq!(
926 proposing_remaining_duration(
927 None,
928 &slot(2),
929 &block_portion,
930 Some(&max_portion),
931 SlotLenienceType::Linear,
932 "test",
933 ),
934 expected,
935 );
936
937 assert_eq!(
939 proposing_remaining_duration(
940 Some(1.into()),
941 &slot_with_head_number(5, 0),
942 &block_portion,
943 Some(&max_portion),
944 SlotLenienceType::Exponential,
945 "test",
946 ),
947 expected,
948 );
949
950 assert_eq!(
952 proposing_remaining_duration(
953 Some(1.into()),
954 &slot(4),
955 &block_portion,
956 Some(&max_portion),
957 SlotLenienceType::Linear,
958 "test",
959 ),
960 expected,
961 );
962
963 assert_eq!(
965 proposing_remaining_duration(
966 Some(1.into()),
967 &slot(2),
968 &block_portion,
969 None,
970 SlotLenienceType::Linear,
971 "test",
972 ),
973 SLOT_DURATION.mul_f32(0.5),
974 );
975 }
976
977 #[derive(PartialEq, Debug)]
978 struct HeadState {
979 head_number: NumberFor<Block>,
980 head_slot: u64,
981 slot_now: NumberFor<Block>,
982 }
983
984 impl HeadState {
985 fn author_block(&mut self) {
986 self.head_number += 1;
988 self.head_slot = self.slot_now;
989 self.slot_now += 1;
991 }
992
993 fn dont_author_block(&mut self) {
994 self.slot_now += 1;
995 }
996 }
997
998 #[test]
999 fn should_never_backoff_when_head_not_advancing() {
1000 let strategy = BackoffAuthoringOnFinalizedHeadLagging::<NumberFor<Block>> {
1001 max_interval: 100,
1002 unfinalized_slack: 5,
1003 authoring_bias: 2,
1004 };
1005
1006 let head_number = 1;
1007 let head_slot = 1;
1008 let finalized_number = 1;
1009 let slot_now = 2;
1010
1011 let should_backoff: Vec<bool> = (slot_now..1000)
1012 .map(|s| {
1013 strategy.should_backoff(
1014 head_number,
1015 head_slot.into(),
1016 finalized_number,
1017 s.into(),
1018 "slots",
1019 )
1020 })
1021 .collect();
1022
1023 let expected: Vec<bool> = (slot_now..1000).map(|_| false).collect();
1025 assert_eq!(should_backoff, expected);
1026 }
1027
1028 #[test]
1029 fn should_stop_authoring_if_blocks_are_still_produced_when_finality_stalled() {
1030 let strategy = BackoffAuthoringOnFinalizedHeadLagging::<NumberFor<Block>> {
1031 max_interval: 100,
1032 unfinalized_slack: 5,
1033 authoring_bias: 2,
1034 };
1035
1036 let mut head_number = 1;
1037 let mut head_slot = 1;
1038 let finalized_number = 1;
1039 let slot_now = 2;
1040
1041 let should_backoff: Vec<bool> = (slot_now..300)
1042 .map(move |s| {
1043 let b = strategy.should_backoff(
1044 head_number,
1045 head_slot.into(),
1046 finalized_number,
1047 s.into(),
1048 "slots",
1049 );
1050 head_number += 1;
1052 head_slot = s;
1053 b
1054 })
1055 .collect();
1056
1057 let expected: Vec<bool> = (slot_now..300).map(|s| s > 8).collect();
1060 assert_eq!(should_backoff, expected);
1061 }
1062
1063 #[test]
1064 fn should_never_backoff_if_max_interval_is_reached() {
1065 let strategy = BackoffAuthoringOnFinalizedHeadLagging::<NumberFor<Block>> {
1066 max_interval: 100,
1067 unfinalized_slack: 5,
1068 authoring_bias: 2,
1069 };
1070
1071 let head_number = 207;
1077 let finalized_number = 1;
1078
1079 let head_slot = 1;
1081 let slot_now = 2;
1082 let max_interval = strategy.max_interval;
1083
1084 let should_backoff: Vec<bool> = (slot_now..200)
1085 .map(|s| {
1086 strategy.should_backoff(
1087 head_number,
1088 head_slot.into(),
1089 finalized_number,
1090 s.into(),
1091 "slots",
1092 )
1093 })
1094 .collect();
1095
1096 let expected: Vec<bool> = (slot_now..200).map(|s| s <= max_interval + head_slot).collect();
1099 assert_eq!(should_backoff, expected);
1100 }
1101
1102 #[test]
1103 fn should_backoff_authoring_when_finality_stalled() {
1104 let param = BackoffAuthoringOnFinalizedHeadLagging {
1105 max_interval: 100,
1106 unfinalized_slack: 5,
1107 authoring_bias: 2,
1108 };
1109
1110 let finalized_number = 2;
1111 let mut head_state = HeadState { head_number: 4, head_slot: 10, slot_now: 11 };
1112
1113 let should_backoff = |head_state: &HeadState| -> bool {
1114 <dyn BackoffAuthoringBlocksStrategy<NumberFor<Block>>>::should_backoff(
1115 ¶m,
1116 head_state.head_number,
1117 head_state.head_slot.into(),
1118 finalized_number,
1119 head_state.slot_now.into(),
1120 "slots",
1121 )
1122 };
1123
1124 let backoff: Vec<bool> = (head_state.slot_now..200)
1125 .map(|_| {
1126 if should_backoff(&head_state) {
1127 head_state.dont_author_block();
1128 true
1129 } else {
1130 head_state.author_block();
1131 false
1132 }
1133 })
1134 .collect();
1135
1136 let expected = [
1138 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,
1145 false, true, true, true, true, true, true, true, false, true, true, true, true, true, true,
1147 true, false, true, true, true, true, true, true, true, true, false, true, true, true, true, true,
1149 true, true, true, false, true, true, true, true, true, true, true, true, true, false, true, true, true, true,
1151 true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true, false, true, true, true,
1153 true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true, true, false, true, true,
1155 true, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true, true, true, false, true,
1157 true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true,
1159 ];
1160
1161 assert_eq!(backoff.as_slice(), &expected[..]);
1162 }
1163
1164 #[test]
1165 fn should_never_wait_more_than_max_interval() {
1166 let param = BackoffAuthoringOnFinalizedHeadLagging {
1167 max_interval: 100,
1168 unfinalized_slack: 5,
1169 authoring_bias: 2,
1170 };
1171
1172 let finalized_number = 2;
1173 let starting_slot = 11;
1174 let mut head_state = HeadState { head_number: 4, head_slot: 10, slot_now: starting_slot };
1175
1176 let should_backoff = |head_state: &HeadState| -> bool {
1177 <dyn BackoffAuthoringBlocksStrategy<NumberFor<Block>>>::should_backoff(
1178 ¶m,
1179 head_state.head_number,
1180 head_state.head_slot.into(),
1181 finalized_number,
1182 head_state.slot_now.into(),
1183 "slots",
1184 )
1185 };
1186
1187 let backoff: Vec<bool> = (head_state.slot_now..40000)
1188 .map(|_| {
1189 if should_backoff(&head_state) {
1190 head_state.dont_author_block();
1191 true
1192 } else {
1193 head_state.author_block();
1194 false
1195 }
1196 })
1197 .collect();
1198
1199 let slots_claimed: Vec<usize> = backoff
1200 .iter()
1201 .enumerate()
1202 .filter(|&(_i, x)| x == &false)
1203 .map(|(i, _x)| i + starting_slot as usize)
1204 .collect();
1205
1206 let last_slot = backoff.len() + starting_slot as usize;
1207 let mut last_two_claimed = slots_claimed.iter().rev().take(2);
1208
1209 let expected_distance = param.max_interval as usize + 1;
1212 assert_eq!(last_slot - last_two_claimed.next().unwrap(), 92);
1213 assert_eq!(last_slot - last_two_claimed.next().unwrap(), 92 + expected_distance);
1214
1215 let intervals: Vec<_> = slots_claimed.windows(2).map(|x| x[1] - x[0]).collect();
1216
1217 assert_eq!(intervals.iter().max(), Some(&expected_distance));
1220
1221 let expected_intervals: Vec<_> =
1223 (0..497).map(|i| (i / 2).clamp(1, expected_distance)).collect();
1224
1225 assert_eq!(intervals, expected_intervals);
1226 }
1227
1228 fn run_until_max_interval(param: BackoffAuthoringOnFinalizedHeadLagging<u64>) -> (u64, u64) {
1229 let finalized_number = 0;
1230 let mut head_state = HeadState { head_number: 0, head_slot: 0, slot_now: 1 };
1231
1232 let should_backoff = |head_state: &HeadState| -> bool {
1233 <dyn BackoffAuthoringBlocksStrategy<NumberFor<Block>>>::should_backoff(
1234 ¶m,
1235 head_state.head_number,
1236 head_state.head_slot.into(),
1237 finalized_number,
1238 head_state.slot_now.into(),
1239 "slots",
1240 )
1241 };
1242
1243 let block_for_max_interval =
1245 param.max_interval * param.authoring_bias + param.unfinalized_slack;
1246
1247 while head_state.head_number < block_for_max_interval {
1248 if should_backoff(&head_state) {
1249 head_state.dont_author_block();
1250 } else {
1251 head_state.author_block();
1252 }
1253 }
1254
1255 let slot_time = 6;
1256 let time_to_reach_limit = slot_time * head_state.slot_now;
1257 (block_for_max_interval, time_to_reach_limit)
1258 }
1259
1260 fn expected_time_to_reach_max_interval(
1269 param: &BackoffAuthoringOnFinalizedHeadLagging<u64>,
1270 ) -> (u64, u64) {
1271 let c = param.unfinalized_slack;
1272 let m = param.authoring_bias;
1273 let x = param.max_interval;
1274 let slot_time = 6;
1275
1276 let block_for_max_interval = x * m + c;
1277
1278 let expected_number_of_slots = (1 + c) + m * x * (x + 1) / 2;
1280 let time_to_reach = expected_number_of_slots * slot_time;
1281
1282 (block_for_max_interval, time_to_reach)
1283 }
1284
1285 #[test]
1286 fn time_to_reach_upper_bound_for_smaller_slack() {
1287 let param = BackoffAuthoringOnFinalizedHeadLagging {
1288 max_interval: 100,
1289 unfinalized_slack: 5,
1290 authoring_bias: 2,
1291 };
1292 let expected = expected_time_to_reach_max_interval(¶m);
1293 let (block_for_max_interval, time_to_reach_limit) = run_until_max_interval(param);
1294 assert_eq!((block_for_max_interval, time_to_reach_limit), expected);
1295 assert_eq!((block_for_max_interval, time_to_reach_limit), (205, 60636));
1297 }
1298
1299 #[test]
1300 fn time_to_reach_upper_bound_for_larger_slack() {
1301 let param = BackoffAuthoringOnFinalizedHeadLagging {
1302 max_interval: 100,
1303 unfinalized_slack: 50,
1304 authoring_bias: 2,
1305 };
1306 let expected = expected_time_to_reach_max_interval(¶m);
1307 let (block_for_max_interval, time_to_reach_limit) = run_until_max_interval(param);
1308 assert_eq!((block_for_max_interval, time_to_reach_limit), expected);
1309 assert_eq!((block_for_max_interval, time_to_reach_limit), (250, 60906));
1310 }
1311}