1use crate::{
74 Application, Automaton, CertifiableAutomaton, CertifiableBlock, Epochable, Relay, Reporter,
75 marshal::{
76 Update,
77 application::{
78 gates::{self, GateOutcome, Gates},
79 validation::{Stage, is_inferred_reproposal_at_certify},
80 },
81 core::{CommitmentFallback, DigestFallback, Mailbox},
82 standard::{
83 Standard, relay,
84 validation::{
85 Decision, ParentCheck, await_and_validate_parent, precheck_epoch_and_reproposal,
86 run_app_verify,
87 },
88 },
89 },
90 simplex::{Plan, types::Context},
91 types::{Epocher, Round},
92};
93use commonware_actor::Feedback;
94use commonware_cryptography::{Digestible, certificate::Scheme};
95use commonware_macros::select;
96use commonware_runtime::{
97 Clock, Metrics, Spawner,
98 telemetry::{
99 metrics::{
100 MetricsExt as _,
101 histogram::{Buckets, Timed},
102 },
103 traces::TracedExt as _,
104 },
105};
106use commonware_utils::{
107 channel::{fallible::OneshotExt, oneshot},
108 sync::TracedAsyncMutex,
109};
110use rand_core::Rng;
111use std::sync::Arc;
112use tracing::{Instrument as _, debug, info_span};
113
114pub struct Deferred<E, S, A, B, ES>
144where
145 E: Rng + Spawner + Metrics + Clock,
146 S: Scheme,
147 A: Application<E>,
148 B: CertifiableBlock,
149 ES: Epocher,
150{
151 context: Arc<TracedAsyncMutex<E>>,
152 application: A,
153 marshal: Mailbox<S, Standard<B>>,
154 epocher: ES,
155 gates: Gates<<B as Digestible>::Digest, B>,
156
157 build_duration: Timed,
158 proposal_parent_fetch_duration: Timed,
159 ancestor_fetch_duration: Timed,
160}
161
162impl<E, S, A, B, ES> Clone for Deferred<E, S, A, B, ES>
163where
164 E: Rng + Spawner + Metrics + Clock,
165 S: Scheme,
166 A: Application<E>,
167 B: CertifiableBlock,
168 ES: Epocher,
169{
170 fn clone(&self) -> Self {
171 Self {
172 context: self.context.clone(),
173 application: self.application.clone(),
174 marshal: self.marshal.clone(),
175 epocher: self.epocher.clone(),
176 gates: self.gates.clone(),
177 build_duration: self.build_duration.clone(),
178 proposal_parent_fetch_duration: self.proposal_parent_fetch_duration.clone(),
179 ancestor_fetch_duration: self.ancestor_fetch_duration.clone(),
180 }
181 }
182}
183
184impl<E, S, A, B, ES> Deferred<E, S, A, B, ES>
185where
186 E: Rng + Spawner + Metrics + Clock,
187 S: Scheme,
188 A: Application<
189 E,
190 Block = B,
191 SigningScheme = S,
192 Context = Context<B::Digest, S::PublicKey>,
193 Input = (),
194 >,
195 B: CertifiableBlock<Context = <A as Application<E>>::Context>,
196 ES: Epocher,
197{
198 pub fn new(context: E, application: A, marshal: Mailbox<S, Standard<B>>, epocher: ES) -> Self {
200 let build_histogram = context.histogram(
201 "build_duration",
202 "Histogram of time taken for the application to build a new block, in seconds",
203 Buckets::LOCAL,
204 );
205 let build_duration = Timed::new(build_histogram);
206 let parent_fetch_histogram = context.histogram(
207 "parent_fetch_duration",
208 "Histogram of time taken to fetch a parent block in propose, in seconds",
209 Buckets::LOCAL,
210 );
211 let proposal_parent_fetch_duration = Timed::new(parent_fetch_histogram);
212 let ancestor_fetch_histogram = context.histogram(
213 "ancestor_fetch_duration",
214 "Histogram of time taken to fetch a block via the ancestry stream, in seconds",
215 Buckets::LOCAL,
216 );
217 let ancestor_fetch_duration = Timed::new(ancestor_fetch_histogram);
218
219 Self {
220 context: Arc::new(TracedAsyncMutex::new("marshal.context", context)),
221 application,
222 marshal,
223 epocher,
224 gates: Gates::new(),
225
226 build_duration,
227 proposal_parent_fetch_duration,
228 ancestor_fetch_duration,
229 }
230 }
231
232 #[inline]
245 async fn deferred_verify(
246 &mut self,
247 context: <Self as Automaton>::Context,
248 block: Arc<B>,
249 parent_request: oneshot::Receiver<Arc<B>>,
250 stage: Stage,
251 ) -> oneshot::Receiver<GateOutcome> {
252 let marshal = self.marshal.clone();
253 let mut application = self.application.clone();
254 let (mut tx, rx) = oneshot::channel();
255 let ancestor_fetch_duration = self.ancestor_fetch_duration.clone();
256 let runtime_context = self
257 .context
258 .lock()
259 .await
260 .child("deferred_verify")
261 .with_attribute("round", context.round);
262 let span = info_span!(
263 "marshal.deferred.verify.deferred",
264 round = %context.round
265 );
266 runtime_context.spawn(move |runtime_context| {
267 async move {
268 let round = context.round;
269
270 let store = stage.store(&marshal, round, Arc::clone(&block));
277 let verify = async {
278 let parent = match await_and_validate_parent(
280 context.parent.1,
281 block.as_ref(),
282 parent_request,
283 &mut tx,
284 )
285 .await
286 {
287 Some(ParentCheck::Valid(parent)) => parent,
288 Some(ParentCheck::Invalid) => return Some(false),
289 None => return None,
290 };
291 run_app_verify(
292 runtime_context,
293 context,
294 Arc::clone(&block),
295 parent,
296 &mut application,
297 &marshal,
298 &mut tx,
299 ancestor_fetch_duration,
300 )
301 .await
302 };
303 let (verdict, durable) = futures::join!(verify, store);
304
305 if let Some(application_valid) = gates::resolve(verdict, durable) {
309 tx.send_lossy(GateOutcome::Ready(application_valid));
310 }
311 }
312 .instrument(span)
313 });
314
315 rx
316 }
317
318 async fn certify_from_embedded_context(
319 &mut self,
320 round: Round,
321 digest: B::Digest,
322 ) -> oneshot::Receiver<bool> {
323 debug!(
338 ?round,
339 ?digest,
340 "subscribing to block for certification using embedded context"
341 );
342 let block_rx = self
343 .marshal
344 .subscribe_by_digest(digest, DigestFallback::FetchByRound { round });
345 let mut marshaled = self.clone();
346 let epocher = self.epocher.clone();
347 let (mut tx, rx) = oneshot::channel();
348 let context = self
349 .context
350 .lock()
351 .await
352 .child("certify")
353 .with_attribute("round", round);
354 context.spawn(move |_| {
355 async move {
356 let block = select! {
357 _ = tx.closed() => {
358 debug!(
359 reason = "consensus dropped receiver",
360 "skipping certification"
361 );
362 return;
363 },
364 result = block_rx => match result {
365 Ok(block) => block,
366 Err(_) => {
367 debug!(
368 ?digest,
369 reason = "failed to fetch block for certification",
370 "skipping certification"
371 );
372 return;
373 }
374 },
375 };
376
377 let embedded_context = block.context();
384 let is_reproposal = is_inferred_reproposal_at_certify(
385 &epocher,
386 block.height(),
387 embedded_context.round,
388 round,
389 );
390 if is_reproposal {
391 if !marshaled.marshal.certified(round, block).await {
395 return;
396 }
397 tx.send_lossy(true);
398 return;
399 }
400
401 let (parent_view, parent_commitment) = embedded_context.parent;
409 let parent_request = marshaled.marshal.subscribe_by_commitment(
410 parent_commitment,
411 CommitmentFallback::FetchByRound {
412 round: Round::new(embedded_context.epoch(), parent_view),
413 },
414 );
415
416 let verify_rx = marshaled
417 .deferred_verify(embedded_context, block, parent_request, Stage::Certified)
418 .await;
419 gates::forward(tx, verify_rx, |result| match result {
420 GateOutcome::Ready(result) => Some(result),
421 GateOutcome::Recover => None,
422 })
423 .await;
424 }
425 .instrument(info_span!(
426 "marshal.deferred.certify.embedded",
427 round = %round,
428 digest = %digest
429 ))
430 });
431 rx
432 }
433
434 #[allow(clippy::async_yields_async)]
435 async fn certify_from_existing_task(
436 &mut self,
437 round: Round,
438 digest: B::Digest,
439 task: oneshot::Receiver<GateOutcome>,
440 ) -> oneshot::Receiver<bool> {
441 self.marshal.hint_notarized(round, digest);
446
447 let mut marshaled = self.clone();
451 let (tx, rx) = oneshot::channel();
452 let context = self
453 .context
454 .lock()
455 .await
456 .child("certify_existing")
457 .with_attribute("round", round);
458 context.spawn(move |_| {
459 gates::drive(tx, task, round, digest, move || async move {
460 marshaled.certify_from_embedded_context(round, digest).await
461 })
462 .instrument(info_span!(
463 "marshal.deferred.certify.existing",
464 round = %round,
465 digest = %digest
466 ))
467 });
468 rx
469 }
470}
471
472impl<E, S, A, B, ES> Automaton for Deferred<E, S, A, B, ES>
473where
474 E: Rng + Spawner + Metrics + Clock,
475 S: Scheme,
476 A: Application<
477 E,
478 Block = B,
479 SigningScheme = S,
480 Context = Context<B::Digest, S::PublicKey>,
481 Input = (),
482 >,
483 B: CertifiableBlock<Context = <A as Application<E>>::Context>,
484 ES: Epocher,
485{
486 type Digest = B::Digest;
487 type Context = Context<Self::Digest, S::PublicKey>;
488
489 #[allow(clippy::async_yields_async)]
503 #[tracing::instrument(name = "marshal.deferred.propose", level = "info", skip_all, fields(round = %consensus_context.round))]
504 async fn propose(
505 &mut self,
506 consensus_context: Context<Self::Digest, S::PublicKey>,
507 ) -> oneshot::Receiver<Self::Digest> {
508 let marshal = self.marshal.clone();
509 let mut application = self.application.clone();
510 let epocher = self.epocher.clone();
511 let gates = self.gates.clone();
512
513 let build_duration = self.build_duration.clone();
515 let proposal_parent_fetch_duration = self.proposal_parent_fetch_duration.clone();
516 let ancestor_fetch_duration = self.ancestor_fetch_duration.clone();
517
518 let (mut tx, rx) = oneshot::channel();
519 let context = self
520 .context
521 .lock()
522 .await
523 .child("propose")
524 .with_attribute("round", consensus_context.round);
525 let span = info_span!(
526 "marshal.deferred.propose.task",
527 round = %consensus_context.round
528 );
529 context.spawn(move |runtime_context| {
530 async move {
531 let last_in_epoch = epocher
549 .last(consensus_context.epoch())
550 .expect("current epoch should exist");
551 if let Some(block) = marshal.get_verified(consensus_context.round).await {
552 let block_context = block.context();
553 let digest = block.digest();
554 let reproposal =
555 digest == consensus_context.parent.1 && block.height() == last_in_epoch;
556 if !reproposal && block_context != consensus_context {
557 debug!(
558 round = ?consensus_context.round,
559 ?consensus_context,
560 ?block_context,
561 "skipping proposal: cached verified block context no longer matches"
562 );
563 return;
564 }
565 debug!(
570 round = ?consensus_context.round,
571 ?digest,
572 reproposal,
573 "reusing verified block from marshal on leader recovery"
574 );
575 gates
576 .stage(
577 consensus_context.round,
578 digest,
579 Arc::new(block),
580 tx,
581 "recovered block",
582 )
583 .await;
584 return;
585 }
586
587 let (parent_view, parent_commitment) = consensus_context.parent;
596 let parent_request = marshal.subscribe_by_commitment(
597 parent_commitment,
598 CommitmentFallback::FetchByRound {
599 round: Round::new(consensus_context.epoch(), parent_view),
600 },
601 );
602
603 let parent_timer = proposal_parent_fetch_duration.timer(&runtime_context);
604 let parent = select! {
605 _ = tx.closed() => {
606 debug!(reason = "consensus dropped receiver", "skipping proposal");
607 return;
608 },
609 result = parent_request => match result {
610 Ok(parent) => parent,
611 Err(_) => {
612 debug!(
613 ?parent_commitment,
614 reason = "failed to fetch parent block",
615 "skipping proposal"
616 );
617 return;
618 }
619 },
620 };
621 parent_timer.observe(&runtime_context);
622
623 if parent.height() == last_in_epoch {
627 let digest = parent.digest();
628 gates
629 .stage(
630 consensus_context.round,
631 digest,
632 parent,
633 tx,
634 "re-proposed boundary block",
635 )
636 .await;
637 return;
638 }
639
640 let ancestor_stream = marshal.ancestor_stream(
641 Arc::new(runtime_context.child("ancestor_stream")),
642 [parent],
643 ancestor_fetch_duration,
644 );
645 let build_request = application
646 .propose(
647 (
648 runtime_context.child("app_propose"),
649 consensus_context.clone(),
650 ),
651 ancestor_stream,
652 (),
653 )
654 .instrument(info_span!(
655 "marshal.deferred.application.propose",
656 round = %consensus_context.round,
657 parent_view = parent_view.traced(),
658 parent = %parent_commitment
659 ));
660
661 let build_timer = build_duration.timer(&runtime_context);
662 let built_block = select! {
663 _ = tx.closed() => {
664 debug!(reason = "consensus dropped receiver", "skipping proposal");
665 return;
666 },
667 result = build_request => match result {
668 Some(block) => block,
669 None => {
670 debug!(
671 ?parent_commitment,
672 reason = "block building failed",
673 "skipping proposal"
674 );
675 return;
676 }
677 },
678 };
679 build_timer.observe(&runtime_context);
680
681 let digest = built_block.digest();
682 gates
683 .stage(
684 consensus_context.round,
685 digest,
686 Arc::new(built_block),
687 tx,
688 "proposed block",
689 )
690 .await;
691 }
692 .instrument(span)
693 });
694 rx
695 }
696
697 #[allow(clippy::async_yields_async)]
698 #[tracing::instrument(name = "marshal.deferred.verify", level = "info", skip_all, fields(round = %context.round, digest = %digest))]
699 async fn verify(
700 &mut self,
701 context: Context<Self::Digest, S::PublicKey>,
702 digest: Self::Digest,
703 ) -> oneshot::Receiver<bool> {
704 let marshal = self.marshal.clone();
705 let mut marshaled = self.clone();
706 let round = context.round;
707
708 let block_request = marshal.subscribe_by_digest(digest, DigestFallback::Wait);
714 let (task_tx, task_rx) = oneshot::channel();
715 self.gates.insert(round, digest, task_rx);
716
717 let (mut tx, rx) = oneshot::channel();
718 let runtime_context = self
719 .context
720 .lock()
721 .await
722 .child("optimistic_verify")
723 .with_attribute("round", round);
724 runtime_context.spawn(move |_| {
725 async move {
726 let parent_request = (digest != context.parent.1).then(|| {
733 let (parent_view, parent_commitment) = context.parent;
734 marshal.subscribe_by_commitment(
735 parent_commitment,
736 CommitmentFallback::FetchByRound {
737 round: Round::new(context.epoch(), parent_view),
738 },
739 )
740 });
741
742 let block = select! {
744 _ = tx.closed() => {
745 debug!(
746 reason = "consensus dropped receiver",
747 "skipping optimistic verification"
748 );
749 return;
750 },
751 result = block_request => match result {
752 Ok(block) => block,
753 Err(_) => {
754 debug!(
755 ?digest,
756 reason = "failed to fetch block for optimistic verification",
757 "skipping optimistic verification"
758 );
759 return;
760 }
761 },
762 };
763
764 let Some(decision) = precheck_epoch_and_reproposal(
773 &marshaled.epocher,
774 &marshal,
775 &context,
776 digest,
777 block,
778 )
779 .await
780 else {
781 return;
782 };
783 let block = match decision {
784 Decision::Complete(valid) => {
785 task_tx.send_lossy(GateOutcome::Ready(valid));
796 tx.send_lossy(valid);
797 return;
798 }
799 Decision::Continue(block) => block,
800 };
801
802 let parent_request =
805 parent_request.expect("non-reproposal has a parent subscription");
806
807 if block.context() != context {
816 debug!(
817 ?context,
818 block_context = ?block.context(),
819 "block-embedded context does not match consensus context during optimistic verification"
820 );
821 task_tx.send_lossy(GateOutcome::Recover);
822 tx.send_lossy(false);
823 return;
824 }
825
826 let deferred_rx = marshaled
835 .deferred_verify(context, block, parent_request, Stage::Verified)
836 .await;
837 tx.send_lossy(true);
838 gates::forward(task_tx, deferred_rx, Some).await;
839 }
840 .instrument(info_span!(
841 "marshal.deferred.verify.optimistic",
842 round = %round,
843 digest = %digest
844 ))
845 });
846 rx
847 }
848}
849
850impl<E, S, A, B, ES> CertifiableAutomaton for Deferred<E, S, A, B, ES>
851where
852 E: Rng + Spawner + Metrics + Clock,
853 S: Scheme,
854 A: Application<
855 E,
856 Block = B,
857 SigningScheme = S,
858 Context = Context<B::Digest, S::PublicKey>,
859 Input = (),
860 >,
861 B: CertifiableBlock<Context = <A as Application<E>>::Context>,
862 ES: Epocher,
863{
864 #[allow(clippy::async_yields_async)]
865 #[tracing::instrument(name = "marshal.deferred.certify", level = "info", skip_all, fields(round = %round, digest = %digest))]
866 async fn certify(&mut self, round: Round, digest: Self::Digest) -> oneshot::Receiver<bool> {
867 self.gates.flush_unrelayed(&self.marshal, round, digest);
868
869 let task = self.gates.take(round, digest);
871 if let Some(task) = task {
872 return self.certify_from_existing_task(round, digest, task).await;
873 }
874
875 self.certify_from_embedded_context(round, digest).await
876 }
877}
878
879impl<E, S, A, B, ES> Relay for Deferred<E, S, A, B, ES>
880where
881 E: Rng + Spawner + Metrics + Clock,
882 S: Scheme,
883 A: Application<E, Block = B, Context = Context<B::Digest, S::PublicKey>>,
884 B: CertifiableBlock<Context = <A as Application<E>>::Context>,
885 ES: Epocher,
886{
887 type Digest = B::Digest;
888 type PublicKey = S::PublicKey;
889 type Plan = Plan<S::PublicKey>;
890
891 fn broadcast(&mut self, commitment: Self::Digest, plan: Plan<S::PublicKey>) -> Feedback {
892 relay::broadcast(&self.gates, &self.marshal, commitment, plan)
893 }
894}
895
896impl<E, S, A, B, ES> Reporter for Deferred<E, S, A, B, ES>
897where
898 E: Rng + Spawner + Metrics + Clock,
899 S: Scheme,
900 A: Application<E, Block = B, Context = Context<B::Digest, S::PublicKey>>
901 + Reporter<Activity = Update<B>>,
902 B: CertifiableBlock<Context = <A as Application<E>>::Context>,
903 ES: Epocher,
904{
905 type Activity = A::Activity;
906
907 fn report(&mut self, update: Self::Activity) -> Feedback {
909 if let Update::Tip(round, _, _) = &update {
911 self.gates.retain_after(round);
912 }
913 self.application.report(update)
914 }
915}
916
917#[cfg(test)]
918mod tests {
919 use super::Deferred;
920 use crate::{
921 Automaton, CertifiableAutomaton, Relay,
922 marshal::mocks::{
923 harness::{
924 B, BLOCKS_PER_EPOCH, Ctx, NAMESPACE, NUM_VALIDATORS, S, StandardHarness,
925 TestHarness, V, default_leader, make_raw_block, setup_network_with_participants,
926 },
927 verifying::{GatedVerifyingApp, MockVerifyingApp},
928 },
929 simplex::{Plan, scheme::bls12381_threshold::vrf as bls12381_threshold_vrf},
930 types::{Epoch, Epocher, FixedEpocher, Height, Round, View},
931 };
932 use commonware_broadcast::Broadcaster;
933 use commonware_cryptography::{
934 Digestible, Hasher as _,
935 certificate::{ConstantProvider, mocks::Fixture},
936 sha256::Sha256,
937 };
938 use commonware_macros::{select, test_traced};
939 use commonware_runtime::{Clock, Runner, Supervisor as _, deterministic};
940 use commonware_utils::{NZUsize, channel::fallible::OneshotExt};
941 use std::time::Duration;
942
943 #[test_traced("INFO")]
944 fn test_certify_lower_view_after_higher_view() {
945 let runner = deterministic::Runner::timed(Duration::from_secs(60));
946 runner.start(|mut context| async move {
947 let Fixture {
948 participants,
949 schemes,
950 ..
951 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
952 let mut oracle = setup_network_with_participants(
953 context.child("network"),
954 NZUsize!(1),
955 participants.clone(),
956 )
957 .await;
958
959 let me = participants[0].clone();
960
961 let setup = StandardHarness::setup_validator(
962 context.child("validator").with_attribute("index", 0),
963 &mut oracle,
964 me.clone(),
965 ConstantProvider::new(schemes[0].clone()),
966 )
967 .await;
968 let marshal = setup.mailbox;
969
970 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
971 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
972
973 let mut marshaled = Deferred::new(
974 context.child("deferred"),
975 mock_app,
976 marshal.clone(),
977 FixedEpocher::new(BLOCKS_PER_EPOCH),
978 );
979
980 let parent = make_raw_block(genesis.digest(), Height::new(1), 100);
982 let parent_digest = parent.digest();
983
984 assert!(
985 marshal
986 .verified(Round::new(Epoch::new(0), View::new(1)), parent.clone())
987 .await
988 );
989
990 let round_a = Round::new(Epoch::new(0), View::new(5));
992 let context_a = Ctx {
993 round: round_a,
994 leader: me.clone(),
995 parent: (View::new(1), parent_digest),
996 };
997 let block_a = B::new::<Sha256>(context_a.clone(), parent_digest, Height::new(2), 200);
998 let commitment_a = StandardHarness::commitment(&block_a);
999 assert!(marshal.verified(round_a, block_a.clone()).await);
1000
1001 let round_b = Round::new(Epoch::new(0), View::new(10));
1003 let context_b = Ctx {
1004 round: round_b,
1005 leader: me.clone(),
1006 parent: (View::new(1), parent_digest),
1007 };
1008 let block_b = B::new::<Sha256>(context_b.clone(), parent_digest, Height::new(2), 300);
1009 let commitment_b = StandardHarness::commitment(&block_b);
1010 assert!(marshal.verified(round_b, block_b.clone()).await);
1011
1012 context.sleep(Duration::from_millis(10)).await;
1013
1014 let _ = marshaled.verify(context_a, commitment_a).await.await;
1016
1017 let _ = marshaled.verify(context_b, commitment_b).await.await;
1019
1020 let certify_b = marshaled.certify(round_b, commitment_b).await;
1022 assert!(
1023 certify_b.await.unwrap(),
1024 "Block B certification should succeed"
1025 );
1026
1027 let certify_a = marshaled.certify(round_a, commitment_a).await;
1029
1030 select! {
1031 result = certify_a => {
1032 assert!(result.unwrap(), "Block A certification should succeed");
1033 },
1034 _ = context.sleep(Duration::from_secs(5)) => {
1035 panic!("Block A certification timed out");
1036 },
1037 }
1038 })
1039 }
1040
1041 #[test_traced("WARN")]
1042 fn test_marshaled_rejects_unsupported_epoch() {
1043 #[derive(Clone)]
1044 struct LimitedEpocher {
1045 inner: FixedEpocher,
1046 max_epoch: u64,
1047 }
1048
1049 impl Epocher for LimitedEpocher {
1050 fn containing(&self, height: Height) -> Option<crate::types::EpochInfo> {
1051 let bounds = self.inner.containing(height)?;
1052 if bounds.epoch().get() > self.max_epoch {
1053 None
1054 } else {
1055 Some(bounds)
1056 }
1057 }
1058
1059 fn first(&self, epoch: Epoch) -> Option<Height> {
1060 if epoch.get() > self.max_epoch {
1061 None
1062 } else {
1063 self.inner.first(epoch)
1064 }
1065 }
1066
1067 fn last(&self, epoch: Epoch) -> Option<Height> {
1068 if epoch.get() > self.max_epoch {
1069 None
1070 } else {
1071 self.inner.last(epoch)
1072 }
1073 }
1074 }
1075
1076 let runner = deterministic::Runner::timed(Duration::from_secs(60));
1077 runner.start(|mut context| async move {
1078 let Fixture {
1079 participants,
1080 schemes,
1081 ..
1082 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1083 let mut oracle = setup_network_with_participants(
1084 context.child("network"),
1085 NZUsize!(1),
1086 participants.clone(),
1087 )
1088 .await;
1089
1090 let me = participants[0].clone();
1091
1092 let setup = StandardHarness::setup_validator(
1093 context.child("validator").with_attribute("index", 0),
1094 &mut oracle,
1095 me.clone(),
1096 ConstantProvider::new(schemes[0].clone()),
1097 )
1098 .await;
1099 let marshal = setup.mailbox;
1100
1101 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1102 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1103 let limited_epocher = LimitedEpocher {
1104 inner: FixedEpocher::new(BLOCKS_PER_EPOCH),
1105 max_epoch: 0,
1106 };
1107
1108 let mut marshaled = Deferred::new(
1109 context.child("deferred"),
1110 mock_app,
1111 marshal.clone(),
1112 limited_epocher,
1113 );
1114
1115 let parent_ctx = Ctx {
1117 round: Round::new(Epoch::zero(), View::new(19)),
1118 leader: default_leader(),
1119 parent: (View::zero(), genesis.digest()),
1120 };
1121 let parent =
1122 B::new::<Sha256>(parent_ctx.clone(), genesis.digest(), Height::new(19), 1000);
1123 let parent_digest = parent.digest();
1124
1125 assert!(
1126 marshal
1127 .clone()
1128 .verified(Round::new(Epoch::zero(), View::new(19)), parent.clone())
1129 .await
1130 );
1131
1132 let unsupported_round = Round::new(Epoch::new(1), View::new(20));
1134 let unsupported_context = Ctx {
1135 round: unsupported_round,
1136 leader: me.clone(),
1137 parent: (View::new(19), parent_digest),
1138 };
1139 let block = B::new::<Sha256>(
1140 unsupported_context.clone(),
1141 parent_digest,
1142 Height::new(20),
1143 2000,
1144 );
1145 let block_commitment = StandardHarness::commitment(&block);
1146
1147 assert!(
1148 marshal
1149 .clone()
1150 .verified(unsupported_round, block.clone())
1151 .await
1152 );
1153
1154 context.sleep(Duration::from_millis(10)).await;
1155
1156 let verify_result = marshaled
1159 .verify(unsupported_context, block_commitment)
1160 .await;
1161
1162 let optimistic_result = verify_result.await;
1164
1165 assert!(
1167 !optimistic_result.unwrap(),
1168 "Optimistic verify should reject block in unsupported epoch"
1169 );
1170 })
1171 }
1172
1173 #[test_traced("WARN")]
1179 fn test_marshaled_rejects_mismatched_context() {
1180 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1181 runner.start(|mut context| async move {
1182 let Fixture {
1183 participants,
1184 schemes,
1185 ..
1186 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1187 let mut oracle = setup_network_with_participants(
1188 context.child("network"),
1189 NZUsize!(1),
1190 participants.clone(),
1191 )
1192 .await;
1193
1194 let me = participants[0].clone();
1195
1196 let setup = StandardHarness::setup_validator(
1197 context.child("validator").with_attribute("index", 0),
1198 &mut oracle,
1199 me.clone(),
1200 ConstantProvider::new(schemes[0].clone()),
1201 )
1202 .await;
1203 let marshal = setup.mailbox;
1204
1205 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1206 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1207
1208 let mut marshaled = Deferred::new(
1209 context.child("deferred"),
1210 mock_app,
1211 marshal.clone(),
1212 FixedEpocher::new(BLOCKS_PER_EPOCH),
1213 );
1214
1215 let parent_ctx = Ctx {
1217 round: Round::new(Epoch::zero(), View::new(1)),
1218 leader: default_leader(),
1219 parent: (View::zero(), genesis.digest()),
1220 };
1221 let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
1222 let parent_commitment = StandardHarness::commitment(&parent);
1223
1224 assert!(
1225 marshal
1226 .clone()
1227 .verified(Round::new(Epoch::zero(), View::new(1)), parent.clone())
1228 .await
1229 );
1230
1231 let round_a = Round::new(Epoch::zero(), View::new(2));
1233 let context_a = Ctx {
1234 round: round_a,
1235 leader: me.clone(),
1236 parent: (View::new(1), parent_commitment),
1237 };
1238 let block_a = B::new::<Sha256>(context_a, parent.digest(), Height::new(2), 200);
1239 let commitment_a = StandardHarness::commitment(&block_a);
1240 assert!(marshal.verified(round_a, block_a).await);
1241
1242 context.sleep(Duration::from_millis(10)).await;
1243
1244 let round_b = Round::new(Epoch::zero(), View::new(3));
1246 let context_b = Ctx {
1247 round: round_b,
1248 leader: participants[1].clone(),
1249 parent: (View::new(1), parent_commitment),
1250 };
1251
1252 let verify_rx = marshaled.verify(context_b, commitment_a).await;
1253 select! {
1254 result = verify_rx => {
1255 assert!(
1256 !result.unwrap(),
1257 "mismatched context hash should be rejected"
1258 );
1259 },
1260 _ = context.sleep(Duration::from_secs(5)) => {
1261 panic!("verify should reject mismatched context hash promptly");
1262 },
1263 }
1264 })
1265 }
1266
1267 #[test_traced("WARN")]
1271 fn test_deferred_certify_recovers_after_verify_receiver_drop() {
1272 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1273 runner.start(|mut context| async move {
1274 let Fixture {
1275 participants,
1276 schemes,
1277 ..
1278 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1279 let mut oracle = setup_network_with_participants(
1280 context.child("network"),
1281 NZUsize!(1),
1282 participants.clone(),
1283 )
1284 .await;
1285
1286 let me = participants[0].clone();
1287 let setup = StandardHarness::setup_validator(
1288 context.child("validator").with_attribute("index", 0),
1289 &mut oracle,
1290 me.clone(),
1291 ConstantProvider::new(schemes[0].clone()),
1292 )
1293 .await;
1294 let marshal = setup.mailbox;
1295
1296 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1297 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1298 let mut marshaled = Deferred::new(
1299 context.child("deferred"),
1300 mock_app,
1301 marshal.clone(),
1302 FixedEpocher::new(BLOCKS_PER_EPOCH),
1303 );
1304
1305 let round = Round::new(Epoch::zero(), View::new(1));
1306 let block_context = Ctx {
1307 round,
1308 leader: me,
1309 parent: (View::zero(), genesis.digest()),
1310 };
1311 let block =
1312 B::new::<Sha256>(block_context.clone(), genesis.digest(), Height::new(1), 100);
1313 let digest = block.digest();
1314
1315 let verify_rx = marshaled.verify(block_context, digest).await;
1316 drop(verify_rx);
1317
1318 context.sleep(Duration::from_millis(10)).await;
1321
1322 assert!(marshal.verified(round, block).await);
1323 let certify_rx = marshaled.certify(round, digest).await;
1324 select! {
1325 result = certify_rx => {
1326 assert!(
1327 result.expect("certify result missing"),
1328 "certify should recover after verify receiver drop"
1329 );
1330 },
1331 _ = context.sleep(Duration::from_secs(5)) => {
1332 panic!("certify should recover promptly after verify drop");
1333 },
1334 }
1335 });
1336 }
1337
1338 #[test_traced("WARN")]
1345 fn test_deferred_store_overlaps_app_verify() {
1346 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1347 runner.start(|mut context| async move {
1348 let Fixture {
1349 participants,
1350 schemes,
1351 ..
1352 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1353 let mut oracle = setup_network_with_participants(
1354 context.child("network"),
1355 NZUsize!(1),
1356 participants.clone(),
1357 )
1358 .await;
1359
1360 let me = participants[0].clone();
1361
1362 let setup = StandardHarness::setup_validator(
1363 context.child("validator").with_attribute("index", 0),
1364 &mut oracle,
1365 me.clone(),
1366 ConstantProvider::new(schemes[0].clone()),
1367 )
1368 .await;
1369 let marshal = setup.mailbox;
1370 let buffer = setup.extra;
1371
1372 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1373 let (mock_app, verify_started, release_verify): (GatedVerifyingApp<B, S>, _, _) =
1374 GatedVerifyingApp::new();
1375 let mut marshaled = Deferred::new(
1376 context.child("deferred"),
1377 mock_app,
1378 marshal.clone(),
1379 FixedEpocher::new(BLOCKS_PER_EPOCH),
1380 );
1381
1382 let parent = make_raw_block(genesis.digest(), Height::new(1), 100);
1386 let parent_digest = parent.digest();
1387
1388 let child_round = Round::new(Epoch::zero(), View::new(2));
1389 let child_ctx = Ctx {
1390 round: child_round,
1391 leader: me,
1392 parent: (View::new(1), parent_digest),
1393 };
1394 let child = B::new::<Sha256>(child_ctx.clone(), parent_digest, Height::new(2), 200);
1395 let child_digest = child.digest();
1396
1397 assert!(
1398 buffer
1399 .broadcast(commonware_p2p::Recipients::Some(vec![]), parent)
1400 .accepted(),
1401 "buffer broadcast for parent should be accepted"
1402 );
1403 assert!(
1404 buffer
1405 .broadcast(commonware_p2p::Recipients::Some(vec![]), child)
1406 .accepted(),
1407 "buffer broadcast for child should be accepted"
1408 );
1409
1410 let optimistic_rx = marshaled.verify(child_ctx, child_digest).await;
1413 assert!(
1414 optimistic_rx
1415 .await
1416 .expect("optimistic verify should resolve"),
1417 "optimistic verify should accept the available block"
1418 );
1419
1420 verify_started
1424 .await
1425 .expect("verify should reach the gated application");
1426 assert!(
1427 marshal.get_block(&child_digest).await.is_some(),
1428 "the store request runs concurrently with app.verify, so the block is locally queryable while verification is still gated"
1429 );
1430
1431 release_verify.send_lossy(());
1433 let certify_rx = marshaled.certify(child_round, child_digest).await;
1434 select! {
1435 result = certify_rx => {
1436 assert!(
1437 result.expect("certify result missing"),
1438 "certify should succeed once verification passes"
1439 );
1440 },
1441 _ = context.sleep(Duration::from_secs(5)) => {
1442 panic!("certify should resolve after verification is released");
1443 },
1444 }
1445 });
1446 }
1447
1448 #[test_traced("WARN")]
1456 fn test_propose_reuses_verified_block_on_restart() {
1457 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1458 runner.start(|mut context| async move {
1459 let Fixture {
1460 participants,
1461 schemes,
1462 ..
1463 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1464 let mut oracle = setup_network_with_participants(
1465 context.child("network"),
1466 NZUsize!(1),
1467 participants.clone(),
1468 )
1469 .await;
1470
1471 let me = participants[0].clone();
1472 let setup = StandardHarness::setup_validator(
1473 context.child("validator").with_attribute("index", 0),
1474 &mut oracle,
1475 me.clone(),
1476 ConstantProvider::new(schemes[0].clone()),
1477 )
1478 .await;
1479 let marshal = setup.mailbox;
1480
1481 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1482 let round = Round::new(Epoch::zero(), View::new(1));
1483 let ctx = Ctx {
1484 round,
1485 leader: me.clone(),
1486 parent: (View::zero(), genesis.digest()),
1487 };
1488 let block_a = B::new::<Sha256>(ctx.clone(), genesis.digest(), Height::new(1), 100);
1489 let digest_a = block_a.digest();
1490 assert!(marshal.verified(round, block_a.clone()).await);
1491
1492 let (mock_app, verify_started, _release_verify): (GatedVerifyingApp<B, S>, _, _) =
1498 GatedVerifyingApp::new();
1499 let mut marshaled = Deferred::new(
1500 context.child("deferred"),
1501 mock_app,
1502 marshal.clone(),
1503 FixedEpocher::new(BLOCKS_PER_EPOCH),
1504 );
1505
1506 let digest_rx = marshaled.propose(ctx).await;
1507 let digest = digest_rx.await.expect("propose must return a digest");
1508 assert_eq!(
1509 digest, digest_a,
1510 "propose must reuse the block marshal already persisted for this round"
1511 );
1512
1513 let _ = marshaled.broadcast(digest, Plan::Propose { round });
1518 let certify_rx = marshaled.certify(round, digest).await;
1519 select! {
1520 result = certify_rx => {
1521 assert!(
1522 result.expect("certify result missing"),
1523 "recovered proposal must certify through the relay handshake"
1524 );
1525 },
1526 _ = verify_started => {
1527 panic!("certifying a recovered proposal must not run app verification");
1528 },
1529 }
1530 });
1531 }
1532
1533 #[test_traced("WARN")]
1540 fn test_propose_reuses_reproposed_boundary_block_on_restart() {
1541 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1542 runner.start(|mut context| async move {
1543 let Fixture {
1544 participants,
1545 schemes,
1546 ..
1547 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1548 let mut oracle = setup_network_with_participants(
1549 context.child("network"),
1550 NZUsize!(1),
1551 participants.clone(),
1552 )
1553 .await;
1554
1555 let me = participants[0].clone();
1556 let setup = StandardHarness::setup_validator(
1557 context.child("validator").with_attribute("index", 0),
1558 &mut oracle,
1559 me.clone(),
1560 ConstantProvider::new(schemes[0].clone()),
1561 )
1562 .await;
1563 let marshal = setup.mailbox;
1564
1565 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1566
1567 let boundary_height = Height::new(BLOCKS_PER_EPOCH.get() - 1);
1570 let boundary_round = Round::new(Epoch::zero(), View::new(boundary_height.get()));
1571 let boundary_block = B::new::<Sha256>(
1572 Ctx {
1573 round: boundary_round,
1574 leader: default_leader(),
1575 parent: (View::zero(), genesis.digest()),
1576 },
1577 genesis.digest(),
1578 boundary_height,
1579 1900,
1580 );
1581 let boundary_digest = boundary_block.digest();
1582 let round = Round::new(Epoch::zero(), View::new(boundary_height.get() + 1));
1583 assert!(marshal.verified(round, boundary_block).await);
1584
1585 let ctx = Ctx {
1586 round,
1587 leader: me.clone(),
1588 parent: (View::new(boundary_height.get()), boundary_digest),
1589 };
1590
1591 let (mock_app, verify_started, _release_verify): (GatedVerifyingApp<B, S>, _, _) =
1595 GatedVerifyingApp::new();
1596 let mut marshaled = Deferred::new(
1597 context.child("deferred"),
1598 mock_app,
1599 marshal.clone(),
1600 FixedEpocher::new(BLOCKS_PER_EPOCH),
1601 );
1602
1603 let digest_rx = marshaled.propose(ctx).await;
1604 let digest = digest_rx.await.expect("propose must return a digest");
1605 assert_eq!(
1606 digest, boundary_digest,
1607 "propose must re-propose the boundary block marshal already persisted for this round"
1608 );
1609
1610 let _ = marshaled.broadcast(digest, Plan::Propose { round });
1611 let certify_rx = marshaled.certify(round, digest).await;
1612 select! {
1613 result = certify_rx => {
1614 assert!(
1615 result.expect("certify result missing"),
1616 "re-proposed boundary block must certify through the relay handshake"
1617 );
1618 },
1619 _ = verify_started => {
1620 panic!("certifying a re-proposed boundary block must not run app verification");
1621 },
1622 }
1623 });
1624 }
1625
1626 #[test_traced("WARN")]
1635 fn test_propose_skips_when_verified_block_context_changed() {
1636 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1637 runner.start(|mut context| async move {
1638 let Fixture {
1639 participants,
1640 schemes,
1641 ..
1642 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1643 let mut oracle = setup_network_with_participants(
1644 context.child("network"),
1645 NZUsize!(1),
1646 participants.clone(),
1647 )
1648 .await;
1649
1650 let me = participants[0].clone();
1651 let setup = StandardHarness::setup_validator(
1652 context.child("validator").with_attribute("index", 0),
1653 &mut oracle,
1654 me.clone(),
1655 ConstantProvider::new(schemes[0].clone()),
1656 )
1657 .await;
1658 let marshal = setup.mailbox;
1659
1660 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1661
1662 let round = Round::new(Epoch::zero(), View::new(2));
1664 let stale_ctx = Ctx {
1665 round,
1666 leader: me.clone(),
1667 parent: (View::zero(), genesis.digest()),
1668 };
1669 let stale_block = B::new::<Sha256>(stale_ctx, genesis.digest(), Height::new(1), 100);
1670 assert!(marshal.verified(round, stale_block).await);
1671
1672 let new_parent_digest = Sha256::hash(&[b"late-certified-parent"]);
1675 let new_ctx = Ctx {
1676 round,
1677 leader: me.clone(),
1678 parent: (View::new(1), new_parent_digest),
1679 };
1680
1681 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1682 let mut marshaled = Deferred::new(
1683 context.child("deferred"),
1684 mock_app,
1685 marshal.clone(),
1686 FixedEpocher::new(BLOCKS_PER_EPOCH),
1687 );
1688
1689 let digest_rx = marshaled.propose(new_ctx).await;
1690 assert!(
1691 digest_rx.await.is_err(),
1692 "propose must drop the receiver when the cached block's context no longer matches"
1693 );
1694 });
1695 }
1696
1697 #[test_traced("WARN")]
1702 fn test_deferred_propose_then_certify_persists_block() {
1703 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1704 runner.start(|mut context| async move {
1705 let Fixture {
1706 participants,
1707 schemes,
1708 ..
1709 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1710 let mut oracle = setup_network_with_participants(
1711 context.child("network"),
1712 NZUsize!(1),
1713 participants.clone(),
1714 )
1715 .await;
1716
1717 let me = participants[0].clone();
1718 let setup = StandardHarness::setup_validator(
1719 context.child("validator").with_attribute("index", 0),
1720 &mut oracle,
1721 me.clone(),
1722 ConstantProvider::new(schemes[0].clone()),
1723 )
1724 .await;
1725 let marshal = setup.mailbox;
1726 let actor_handle = setup.actor_handle;
1727
1728 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1729
1730 let parent_round = Round::new(Epoch::zero(), View::new(1));
1732 let parent_ctx = Ctx {
1733 round: parent_round,
1734 leader: default_leader(),
1735 parent: (View::zero(), genesis.digest()),
1736 };
1737 let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
1738 let parent_digest = parent.digest();
1739 assert!(marshal.verified(parent_round, parent).await);
1740
1741 let round = Round::new(Epoch::zero(), View::new(2));
1743 let ctx = Ctx {
1744 round,
1745 leader: me.clone(),
1746 parent: (View::new(1), parent_digest),
1747 };
1748 let child = B::new::<Sha256>(ctx.clone(), parent_digest, Height::new(2), 200);
1749 let child_digest = child.digest();
1750 let mock_app: MockVerifyingApp<B, S> =
1751 MockVerifyingApp::new().with_propose_result(child);
1752 let mut marshaled = Deferred::new(
1753 context.child("deferred"),
1754 mock_app,
1755 marshal.clone(),
1756 FixedEpocher::new(BLOCKS_PER_EPOCH),
1757 );
1758
1759 let digest = marshaled
1760 .propose(ctx)
1761 .await
1762 .await
1763 .expect("propose must return a digest");
1764 assert_eq!(
1765 digest, child_digest,
1766 "propose must return the built block's digest"
1767 );
1768
1769 assert!(
1771 marshaled
1772 .certify(round, child_digest)
1773 .await
1774 .await
1775 .expect("certify result missing"),
1776 "certify must succeed for the leader's own proposal"
1777 );
1778
1779 actor_handle.abort();
1781 drop(marshaled);
1782 drop(marshal);
1783
1784 let setup2 = StandardHarness::setup_validator(
1785 context
1786 .child("validator_restart")
1787 .with_attribute("index", 0),
1788 &mut oracle,
1789 me,
1790 ConstantProvider::new(schemes[0].clone()),
1791 )
1792 .await;
1793 let marshal2 = setup2.mailbox;
1794
1795 assert!(
1796 marshal2.get_block(&child_digest).await.is_some(),
1797 "certify resolved true for the leader's own proposal so the block must be durable"
1798 );
1799 });
1800 }
1801
1802 struct EquivocationFixture {
1811 marshaled: Deferred<deterministic::Context, S, MockVerifyingApp<B, S>, B, FixedEpocher>,
1812 round: Round,
1813 digest: <B as Digestible>::Digest,
1814 embedded_ctx: Ctx,
1815 equivocating_ctx: Ctx,
1816 _extra: <StandardHarness as TestHarness>::ValidatorExtra,
1817 }
1818
1819 async fn equivocation_fixture(
1820 context: &mut deterministic::Context,
1821 app: MockVerifyingApp<B, S>,
1822 ) -> EquivocationFixture {
1823 let Fixture {
1824 participants,
1825 schemes,
1826 ..
1827 } = bls12381_threshold_vrf::fixture::<V, _>(context, NAMESPACE, NUM_VALIDATORS);
1828 let mut oracle = setup_network_with_participants(
1829 context.child("network"),
1830 NZUsize!(1),
1831 participants.clone(),
1832 )
1833 .await;
1834
1835 let me = participants[0].clone();
1836 let setup = StandardHarness::setup_validator(
1837 context.child("validator").with_attribute("index", 0),
1838 &mut oracle,
1839 me,
1840 ConstantProvider::new(schemes[0].clone()),
1841 )
1842 .await;
1843 let marshal = setup.mailbox;
1844 let buffer = setup.extra;
1845
1846 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1847 let leader = participants[1].clone();
1848
1849 let certified_round = Round::new(Epoch::zero(), View::new(1));
1851 let certified_ctx = Ctx {
1852 round: certified_round,
1853 leader: default_leader(),
1854 parent: (View::zero(), genesis.digest()),
1855 };
1856 let certified = B::new::<Sha256>(certified_ctx, genesis.digest(), Height::new(1), 100);
1857 let certified_digest = certified.digest();
1858 assert!(marshal.verified(certified_round, certified).await);
1859
1860 let notarized_round = Round::new(Epoch::zero(), View::new(2));
1863 let notarized_ctx = Ctx {
1864 round: notarized_round,
1865 leader: leader.clone(),
1866 parent: (View::new(1), certified_digest),
1867 };
1868 let notarized = B::new::<Sha256>(notarized_ctx, certified_digest, Height::new(2), 200);
1869 let notarized_digest = notarized.digest();
1870 assert!(marshal.verified(notarized_round, notarized).await);
1871
1872 let round = Round::new(Epoch::zero(), View::new(3));
1875 let embedded_ctx = Ctx {
1876 round,
1877 leader: leader.clone(),
1878 parent: (View::new(2), notarized_digest),
1879 };
1880 let block = B::new::<Sha256>(embedded_ctx.clone(), notarized_digest, Height::new(3), 300);
1881 let digest = block.digest();
1882 assert!(
1883 buffer
1884 .broadcast(commonware_p2p::Recipients::Some(vec![]), block)
1885 .accepted(),
1886 "buffer broadcast for the candidate should be accepted"
1887 );
1888
1889 let equivocating_ctx = Ctx {
1890 round,
1891 leader,
1892 parent: (View::new(1), certified_digest),
1893 };
1894
1895 let marshaled = Deferred::new(
1896 context.child("deferred"),
1897 app,
1898 marshal,
1899 FixedEpocher::new(BLOCKS_PER_EPOCH),
1900 );
1901 context.sleep(Duration::from_millis(10)).await;
1902
1903 EquivocationFixture {
1904 marshaled,
1905 round,
1906 digest,
1907 embedded_ctx,
1908 equivocating_ctx,
1909 _extra: buffer,
1910 }
1911 }
1912
1913 #[test_traced("WARN")]
1929 fn test_certify_not_poisoned_by_equivocating_parent_verify() {
1930 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1931 runner.start(|mut context| async move {
1932 let mut fixture = equivocation_fixture(&mut context, MockVerifyingApp::new()).await;
1933
1934 let verify_rx = fixture
1935 .marshaled
1936 .verify(fixture.equivocating_ctx.clone(), fixture.digest)
1937 .await;
1938 assert!(
1939 !verify_rx.await.expect("verify result missing"),
1940 "the equivocating proposal must not be notarized"
1941 );
1942
1943 let certify_rx = fixture
1944 .marshaled
1945 .certify(fixture.round, fixture.digest)
1946 .await;
1947 select! {
1948 result = certify_rx => {
1949 assert!(
1950 result.expect("certify result missing"),
1951 "certify of the notarized digest must not adopt the verdict computed under the equivocating context"
1952 );
1953 },
1954 _ = context.sleep(Duration::from_secs(5)) => {
1955 panic!("certify should resolve promptly");
1956 },
1957 }
1958 });
1959 }
1960
1961 #[test_traced("WARN")]
1966 fn test_certify_honors_application_rejection() {
1967 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1968 runner.start(|mut context| async move {
1969 let mut fixture =
1970 equivocation_fixture(&mut context, MockVerifyingApp::with_verify_result(false))
1971 .await;
1972
1973 let verify_rx = fixture
1974 .marshaled
1975 .verify(fixture.embedded_ctx.clone(), fixture.digest)
1976 .await;
1977 assert!(
1978 verify_rx.await.expect("verify result missing"),
1979 "optimistic verify accepts an available block with a matching context"
1980 );
1981
1982 let certify_rx = fixture
1983 .marshaled
1984 .certify(fixture.round, fixture.digest)
1985 .await;
1986 select! {
1987 result = certify_rx => {
1988 assert!(
1989 !result.expect("certify result missing"),
1990 "certify must propagate the application rejection"
1991 );
1992 },
1993 _ = context.sleep(Duration::from_secs(5)) => {
1994 panic!("certify should resolve promptly");
1995 },
1996 }
1997 });
1998 }
1999
2000 #[test_traced("WARN")]
2004 fn test_certify_without_prior_verify_honors_application_rejection() {
2005 let runner = deterministic::Runner::timed(Duration::from_secs(30));
2006 runner.start(|mut context| async move {
2007 let mut fixture =
2008 equivocation_fixture(&mut context, MockVerifyingApp::with_verify_result(false))
2009 .await;
2010
2011 let certify_rx = fixture
2014 .marshaled
2015 .certify(fixture.round, fixture.digest)
2016 .await;
2017 select! {
2018 result = certify_rx => {
2019 assert!(
2020 !result.expect("certify result missing"),
2021 "certify must propagate the application rejection"
2022 );
2023 },
2024 _ = context.sleep(Duration::from_secs(5)) => {
2025 panic!("certify should resolve promptly");
2026 },
2027 }
2028 });
2029 }
2030}