1use crate::{
84 Application, Automaton, Block, CertifiableAutomaton, CertifiableBlock, Epochable, Handoff,
85 Heightable, Relay, Reporter,
86 marshal::{
87 ancestry::Ancestry,
88 application::{
89 build::{self, Resolved},
90 gates::{self, GateOutcome, Gates},
91 relay,
92 validation::{Stage, is_inferred_reproposal_at_certify},
93 },
94 core::{CommitmentFallback, DigestFallback, Mailbox},
95 standard::{
96 Standard,
97 validation::{
98 Decision, ParentCheck, await_and_validate_parent, precheck_epoch_and_reproposal,
99 run_app_verify,
100 },
101 },
102 },
103 simplex::{
104 Plan,
105 types::{Activity, Context},
106 },
107 types::{Epocher, Round, View},
108};
109use commonware_actor::Feedback;
110use commonware_codec::Codec;
111use commonware_cryptography::{Digested, Digestible, certificate::Scheme};
112use commonware_macros::select;
113use commonware_runtime::{
114 Clock, Metrics, Spawner,
115 telemetry::{
116 metrics::{
117 MetricsExt as _,
118 histogram::{Buckets, Timed},
119 },
120 traces::TracedExt as _,
121 },
122};
123use commonware_utils::channel::{fallible::OneshotExt, oneshot};
124use std::{
125 future::{Future, ready},
126 sync::Arc,
127};
128use tracing::{Instrument as _, debug, info_span};
129
130pub struct Deferred<E, S, A, B, ES>
160where
161 S: Scheme,
162 A: Application<E>,
163 B: CertifiableBlock + Codec,
164 ES: Epocher,
165{
166 context: Arc<E>,
167 application: A,
168 marshal: Mailbox<S, Standard<B>>,
169 epocher: ES,
170 gates: Gates<<B as Digestible>::Digest, Digested<B>>,
171
172 build_duration: Timed,
173 proposal_parent_fetch_duration: Timed,
174 ancestor_fetch_duration: Timed,
175}
176
177impl<E, S, A, B, ES> Clone for Deferred<E, S, A, B, ES>
178where
179 S: Scheme,
180 A: Application<E>,
181 B: CertifiableBlock + Codec,
182 ES: Epocher,
183{
184 fn clone(&self) -> Self {
185 Self {
186 context: self.context.clone(),
187 application: self.application.clone(),
188 marshal: self.marshal.clone(),
189 epocher: self.epocher.clone(),
190 gates: self.gates.clone(),
191 build_duration: self.build_duration.clone(),
192 proposal_parent_fetch_duration: self.proposal_parent_fetch_duration.clone(),
193 ancestor_fetch_duration: self.ancestor_fetch_duration.clone(),
194 }
195 }
196}
197
198impl<E, S, A, B, ES> Deferred<E, S, A, B, ES>
199where
200 E: Spawner + Metrics + Clock,
201 S: Scheme,
202 A: Application<
203 E,
204 Block = B,
205 SigningScheme = S,
206 Context = Context<B::Digest, S::PublicKey>,
207 Input = (),
208 >,
209 B: CertifiableBlock<Context = <A as Application<E>>::Context> + Codec,
210 ES: Epocher,
211{
212 pub fn new(context: E, application: A, marshal: Mailbox<S, Standard<B>>, epocher: ES) -> Self {
214 let build_histogram = context.histogram(
215 "build_duration",
216 "Histogram of time taken for the application to build a new block, in seconds",
217 Buckets::LOCAL,
218 );
219 let build_duration = Timed::new(build_histogram);
220 let parent_fetch_histogram = context.histogram(
221 "parent_fetch_duration",
222 "Histogram of time taken to fetch a parent block in proposal, in seconds",
223 Buckets::LOCAL,
224 );
225 let proposal_parent_fetch_duration = Timed::new(parent_fetch_histogram);
226 let ancestor_fetch_histogram = context.histogram(
227 "ancestor_fetch_duration",
228 "Histogram of time taken to fetch a block via the ancestry stream, in seconds",
229 Buckets::LOCAL,
230 );
231 let ancestor_fetch_duration = Timed::new(ancestor_fetch_histogram);
232
233 Self {
234 context: Arc::new(context),
235 application,
236 marshal,
237 epocher,
238 gates: Gates::new(),
239
240 build_duration,
241 proposal_parent_fetch_duration,
242 ancestor_fetch_duration,
243 }
244 }
245
246 #[inline]
259 fn deferred_verify(
260 &self,
261 context: <Self as Automaton>::Context,
262 block: Digested<B>,
263 parent_request: oneshot::Receiver<Digested<B>>,
264 stage: Stage,
265 ) -> oneshot::Receiver<GateOutcome> {
266 let marshal = self.marshal.clone();
267 let mut application = self.application.clone();
268 let (mut tx, rx) = oneshot::channel();
269 let ancestor_fetch_duration = self.ancestor_fetch_duration.clone();
270 let runtime_context = self
271 .context
272 .child("deferred_verify")
273 .with_attribute("round", context.round);
274 let span = info_span!(
275 "marshal.deferred.verify.deferred",
276 round = %context.round
277 );
278 runtime_context.spawn(move |runtime_context| {
279 async move {
280 let round = context.round;
281
282 let store = stage.store(&marshal, round, block.clone());
289 let verify = async {
290 let parent = match await_and_validate_parent(
292 context.parent.1,
293 &block,
294 parent_request,
295 &mut tx,
296 )
297 .await
298 {
299 Some(ParentCheck::Valid(parent)) => parent,
300 Some(ParentCheck::Invalid) => return Some(false),
301 None => return None,
302 };
303 run_app_verify(
304 runtime_context,
305 context,
306 block.clone(),
307 parent,
308 &mut application,
309 &marshal,
310 &mut tx,
311 ancestor_fetch_duration,
312 )
313 .await
314 };
315 let (verdict, durable) = futures::join!(verify, store);
316
317 if let Some(application_valid) = gates::resolve(verdict, durable) {
321 tx.send_lossy(GateOutcome::Ready(application_valid));
322 }
323 }
324 .instrument(span)
325 });
326
327 rx
328 }
329
330 fn certify_from_embedded_context(
331 &self,
332 round: Round,
333 digest: B::Digest,
334 ) -> oneshot::Receiver<bool> {
335 debug!(
350 ?round,
351 ?digest,
352 "subscribing to block for certification using embedded context"
353 );
354 let block_rx = self
355 .marshal
356 .subscribe_by_digest(digest, DigestFallback::FetchByRound { round });
357 let marshaled = self.clone();
358 let epocher = self.epocher.clone();
359 let (mut tx, rx) = oneshot::channel();
360 let context = self.context.child("certify").with_attribute("round", round);
361 context.spawn(move |_| {
362 async move {
363 let block = select! {
364 _ = tx.closed() => {
365 debug!(
366 reason = "consensus dropped receiver",
367 "skipping certification"
368 );
369 return;
370 },
371 result = block_rx => match result {
372 Ok(block) => block,
373 Err(_) => {
374 debug!(
375 ?digest,
376 reason = "failed to fetch block for certification",
377 "skipping certification"
378 );
379 return;
380 }
381 },
382 };
383
384 let embedded_context = block.context();
391 let is_reproposal = is_inferred_reproposal_at_certify(
392 &epocher,
393 block.height(),
394 embedded_context.round,
395 round,
396 );
397 if is_reproposal {
398 if !marshaled.marshal.certified(round, block).await {
402 return;
403 }
404 tx.send_lossy(true);
405 return;
406 }
407
408 let (parent_view, parent_commitment) = embedded_context.parent;
416 let parent_request = marshaled.marshal.subscribe_by_commitment(
417 parent_commitment,
418 CommitmentFallback::FetchByRound {
419 round: Round::new(embedded_context.epoch(), parent_view),
420 },
421 );
422
423 let verify_rx = marshaled.deferred_verify(
424 embedded_context,
425 block,
426 parent_request,
427 Stage::Certified,
428 );
429 gates::forward(tx, verify_rx, |result| match result {
430 GateOutcome::Ready(result) => Some(result),
431 GateOutcome::Recover => None,
432 })
433 .await;
434 }
435 .instrument(info_span!(
436 "marshal.deferred.certify.embedded",
437 round = %round,
438 digest = %digest
439 ))
440 });
441 rx
442 }
443 fn certify_from_existing_task(
444 &self,
445 round: Round,
446 digest: B::Digest,
447 task: oneshot::Receiver<GateOutcome>,
448 ) -> oneshot::Receiver<bool> {
449 self.marshal.hint_notarized(round, digest);
454
455 let marshaled = self.clone();
459 let (tx, rx) = oneshot::channel();
460 let context = self
461 .context
462 .child("certify_existing")
463 .with_attribute("round", round);
464 context.spawn(move |_| {
465 gates::drive(tx, task, round, digest, move || {
466 marshaled.certify_from_embedded_context(round, digest)
467 })
468 .instrument(info_span!(
469 "marshal.deferred.certify.existing",
470 round = %round,
471 digest = %digest
472 ))
473 });
474 rx
475 }
476
477 fn checks(
484 &self,
485 consensus_context: Context<B::Digest, S::PublicKey>,
486 ) -> impl Future<Output = Resolved<B::Digest, Digested<B>, impl Ancestry<B>>> + Send + 'static
487 {
488 let context = self.context.clone();
489 let marshal = self.marshal.clone();
490 let epocher = self.epocher.clone();
491 let proposal_parent_fetch_duration = self.proposal_parent_fetch_duration.clone();
492 let ancestor_fetch_duration = self.ancestor_fetch_duration.clone();
493 let build_duration = self.build_duration.clone();
494 async move {
495 let (parent_view, parent_commitment) = consensus_context.parent;
504 let parent_request = marshal.subscribe_by_commitment(
505 parent_commitment,
506 CommitmentFallback::FetchByRound {
507 round: Round::new(consensus_context.epoch(), parent_view),
508 },
509 );
510 let parent_timer = proposal_parent_fetch_duration.timer(&*context);
511 let Ok(parent) = parent_request.await else {
512 debug!(
513 ?parent_commitment,
514 reason = "failed to fetch parent block",
515 "skipping proposal"
516 );
517 return Resolved::Skip;
518 };
519 parent_timer.observe(&*context);
520
521 if parent_view != View::zero() && parent.parent() != parent.context().parent.1 {
532 debug!(
533 ?parent_commitment,
534 reason = "parent does not link to its context parent",
535 "skipping proposal"
536 );
537 return Resolved::Skip;
538 }
539
540 let last_in_epoch = epocher
544 .last(consensus_context.epoch())
545 .expect("current epoch should exist");
546 if parent.height() == last_in_epoch {
547 return Resolved::Reuse(parent.digest(), parent);
548 }
549
550 let ancestor_stream =
551 marshal.ancestor_stream(context.clone(), [parent], ancestor_fetch_duration);
552 Resolved::Build(ancestor_stream, build_duration.timer(&*context), ())
553 }
554 }
555}
556
557impl<E, S, A, B, ES> Automaton for Deferred<E, S, A, B, ES>
558where
559 E: Spawner + Metrics + Clock,
560 S: Scheme,
561 A: Application<
562 E,
563 Block = B,
564 SigningScheme = S,
565 Context = Context<B::Digest, S::PublicKey>,
566 Input = (),
567 >,
568 B: CertifiableBlock<Context = <A as Application<E>>::Context> + Codec,
569 ES: Epocher,
570{
571 type Digest = B::Digest;
572 type Context = Context<Self::Digest, S::PublicKey>;
573
574 #[allow(clippy::async_yields_async)]
588 #[tracing::instrument(name = "marshal.deferred.propose", level = "info", skip_all, fields(round = %consensus_context.round))]
589 async fn propose(
590 &mut self,
591 consensus_context: Context<Self::Digest, S::PublicKey>,
592 ) -> oneshot::Receiver<Self::Digest> {
593 let checks = self.checks(consensus_context.clone());
594 let span = info_span!(
595 "marshal.deferred.propose.task",
596 round = %consensus_context.round
597 );
598 let round = consensus_context.round;
599 let (parent_view, parent_commitment) = consensus_context.parent;
600 build::propose(
601 self.context.as_ref(),
602 &self.application,
603 self.gates.clone(),
604 consensus_context,
605 checks,
606 span,
607 move || {
608 info_span!(
609 "marshal.deferred.application.propose",
610 round = %round,
611 parent_view = parent_view.traced(),
612 parent = %parent_commitment
613 )
614 },
615 |block, ()| ready((block.digest(), block)),
616 )
617 }
618
619 #[allow(clippy::async_yields_async)]
620 #[tracing::instrument(name = "marshal.deferred.verify", level = "info", skip_all, fields(round = %context.round, digest = %digest))]
621 async fn verify(
622 &mut self,
623 context: Context<Self::Digest, S::PublicKey>,
624 digest: Self::Digest,
625 ) -> oneshot::Receiver<bool> {
626 let marshal = self.marshal.clone();
627 let marshaled = self.clone();
628 let round = context.round;
629
630 let block_request = marshal.subscribe_by_digest(digest, DigestFallback::Wait);
636 let (task_tx, task_rx) = oneshot::channel();
637 self.gates.insert(round, digest, task_rx);
638
639 let (mut tx, rx) = oneshot::channel();
640 let runtime_context = self
641 .context
642 .child("optimistic_verify")
643 .with_attribute("round", round);
644 runtime_context.spawn(move |_| {
645 async move {
646 let parent_request = (digest != context.parent.1).then(|| {
653 let (parent_view, parent_commitment) = context.parent;
654 marshal.subscribe_by_commitment(
655 parent_commitment,
656 CommitmentFallback::FetchByRound {
657 round: Round::new(context.epoch(), parent_view),
658 },
659 )
660 });
661
662 let block = select! {
664 _ = tx.closed() => {
665 debug!(
666 reason = "consensus dropped receiver",
667 "skipping optimistic verification"
668 );
669 return;
670 },
671 result = block_request => match result {
672 Ok(block) => block,
673 Err(_) => {
674 debug!(
675 ?digest,
676 reason = "failed to fetch block for optimistic verification",
677 "skipping optimistic verification"
678 );
679 return;
680 }
681 },
682 };
683
684 let Some(decision) = precheck_epoch_and_reproposal(
693 &marshaled.epocher,
694 &marshal,
695 &context,
696 digest,
697 block,
698 )
699 .await
700 else {
701 return;
702 };
703 let block = match decision {
704 Decision::Complete(valid) => {
705 task_tx.send_lossy(GateOutcome::Ready(valid));
716 tx.send_lossy(valid);
717 return;
718 }
719 Decision::Continue(block) => block,
720 };
721
722 let parent_request =
725 parent_request.expect("non-reproposal has a parent subscription");
726
727 if block.context() != context {
736 debug!(
737 ?context,
738 block_context = ?block.context(),
739 "block-embedded context does not match consensus context during optimistic verification"
740 );
741 task_tx.send_lossy(GateOutcome::Recover);
742 tx.send_lossy(false);
743 return;
744 }
745
746 let deferred_rx = marshaled
755 .deferred_verify(context, block, parent_request, Stage::Verified);
756 tx.send_lossy(true);
757 gates::forward(task_tx, deferred_rx, Some).await;
758 }
759 .instrument(info_span!(
760 "marshal.deferred.verify.optimistic",
761 round = %round,
762 digest = %digest
763 ))
764 });
765 rx
766 }
767}
768
769impl<E, S, A, B, ES> CertifiableAutomaton for Deferred<E, S, A, B, ES>
770where
771 E: Spawner + Metrics + Clock,
772 S: Scheme,
773 A: Application<
774 E,
775 Block = B,
776 SigningScheme = S,
777 Context = Context<B::Digest, S::PublicKey>,
778 Input = (),
779 >,
780 B: CertifiableBlock<Context = <A as Application<E>>::Context> + Codec,
781 ES: Epocher,
782{
783 #[allow(clippy::async_yields_async)]
789 #[tracing::instrument(name = "marshal.deferred.prepare", level = "info", skip_all, fields(round = %consensus_context.round))]
790 async fn prepare(
791 &mut self,
792 consensus_context: Context<Self::Digest, S::PublicKey>,
793 ) -> oneshot::Receiver<Handoff<Self::Digest>> {
794 let checks = self.checks(consensus_context.clone());
795 let span = info_span!(
796 "marshal.deferred.prepare.task",
797 round = %consensus_context.round
798 );
799 let round = consensus_context.round;
800 let (parent_view, parent_commitment) = consensus_context.parent;
801 build::prepare(
802 self.context.as_ref(),
803 &self.application,
804 self.gates.clone(),
805 consensus_context,
806 checks,
807 span,
808 move || {
809 info_span!(
810 "marshal.deferred.application.prepare",
811 round = %round,
812 parent_view = parent_view.traced(),
813 parent = %parent_commitment
814 )
815 },
816 |block, ()| ready((block.digest(), block)),
817 )
818 }
819
820 #[allow(clippy::async_yields_async)]
821 #[tracing::instrument(name = "marshal.deferred.certify", level = "info", skip_all, fields(round = %round, digest = %digest))]
822 async fn certify(&mut self, round: Round, digest: Self::Digest) -> oneshot::Receiver<bool> {
823 let task = self.gates.claim(round, digest, |block, ack| {
825 self.marshal.verified_deferred(round, block, ack)
826 });
827 if let Some(task) = task {
828 return self.certify_from_existing_task(round, digest, task);
829 }
830
831 self.certify_from_embedded_context(round, digest)
832 }
833}
834
835impl<E, S, A, B, ES> Relay for Deferred<E, S, A, B, ES>
836where
837 E: Send + Sync + 'static,
838 S: Scheme,
839 A: Application<E, Block = B, Context = Context<B::Digest, S::PublicKey>>,
840 B: CertifiableBlock<Context = <A as Application<E>>::Context> + Codec,
841 ES: Epocher,
842{
843 type Digest = B::Digest;
844 type PublicKey = S::PublicKey;
845 type Plan = Plan<S::PublicKey>;
846
847 fn broadcast(&mut self, commitment: Self::Digest, plan: Plan<S::PublicKey>) -> Feedback {
848 relay::broadcast(&self.gates, &self.marshal, commitment, plan)
849 }
850}
851
852impl<E, S, A, B, ES> Reporter for Deferred<E, S, A, B, ES>
860where
861 E: Send + Sync + 'static,
862 S: Scheme,
863 A: Application<E, Block = B, Context = Context<B::Digest, S::PublicKey>>,
864 B: CertifiableBlock<Context = <A as Application<E>>::Context> + Codec,
865 ES: Epocher,
866{
867 type Activity = Activity<S, B::Digest>;
868
869 fn report(&mut self, activity: Self::Activity) -> Feedback {
870 let retired = match &activity {
871 Activity::Finalization(finalization) => finalization.round(),
872 activity => Round::new(activity.epoch(), View::zero()),
873 };
874 self.gates.retain_after(retired);
875 self.marshal.report(activity)
876 }
877}
878
879#[cfg(test)]
880mod tests {
881 use super::Deferred;
882 use crate::{
883 Automaton, CertifiableAutomaton, Relay,
884 marshal::{
885 application::gates::Gates,
886 mocks::{
887 harness::{
888 B, BLOCKS_PER_EPOCH, Ctx, D, NAMESPACE, NUM_VALIDATORS, S, StandardHarness,
889 TestHarness, V, default_leader, make_raw_block,
890 setup_network_with_participants,
891 },
892 verifying::{GatedVerifyingApp, MockVerifyingApp},
893 },
894 standard::tests::ready,
895 },
896 simplex::{Plan, scheme::bls12381_threshold::vrf as bls12381_threshold_vrf},
897 types::{Epoch, Epocher, FixedEpocher, Height, Round, View},
898 };
899 use commonware_broadcast::Broadcaster;
900 use commonware_cryptography::{
901 Digested, Digestible, Hasher as _,
902 certificate::{ConstantProvider, mocks::Fixture},
903 sha256::Sha256,
904 };
905 use commonware_macros::{select, test_traced};
906 use commonware_runtime::{Clock, Runner, Supervisor as _, deterministic};
907 use commonware_utils::{NZUsize, channel::fallible::OneshotExt};
908 use std::time::Duration;
909
910 #[test_traced("INFO")]
911 fn test_certify_lower_view_after_higher_view() {
912 let runner = deterministic::Runner::timed(Duration::from_secs(60));
913 runner.start(|mut context| async move {
914 let Fixture {
915 participants,
916 schemes,
917 ..
918 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
919 let mut oracle = setup_network_with_participants(
920 context.child("network"),
921 NZUsize!(1),
922 participants.clone(),
923 1024 * 1024,
924 )
925 .await;
926
927 let me = participants[0].clone();
928
929 let setup = StandardHarness::setup_validator(
930 context.child("validator").with_attribute("index", 0),
931 &mut oracle,
932 me.clone(),
933 ConstantProvider::new(schemes[0].clone()),
934 )
935 .await;
936 let marshal = setup.mailbox;
937
938 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
939 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
940
941 let mut marshaled = Deferred::new(
942 context.child("deferred"),
943 mock_app,
944 marshal.clone(),
945 FixedEpocher::new(BLOCKS_PER_EPOCH),
946 );
947
948 let parent = make_raw_block(genesis.digest(), Height::new(1), 100);
950 let parent_digest = parent.digest();
951
952 assert!(
953 marshal
954 .verified(
955 Round::new(Epoch::new(0), View::new(1)),
956 ready(parent.clone())
957 )
958 .await
959 );
960
961 let round_a = Round::new(Epoch::new(0), View::new(5));
963 let context_a = Ctx {
964 round: round_a,
965 leader: me.clone(),
966 parent: (View::new(1), parent_digest),
967 };
968 let block_a = B::new::<Sha256>(context_a.clone(), parent_digest, Height::new(2), 200);
969 let commitment_a = block_a.digest();
970 assert!(marshal.verified(round_a, ready(block_a.clone())).await);
971
972 let round_b = Round::new(Epoch::new(0), View::new(10));
974 let context_b = Ctx {
975 round: round_b,
976 leader: me.clone(),
977 parent: (View::new(1), parent_digest),
978 };
979 let block_b = B::new::<Sha256>(context_b.clone(), parent_digest, Height::new(2), 300);
980 let commitment_b = block_b.digest();
981 assert!(marshal.verified(round_b, ready(block_b.clone())).await);
982
983 context.sleep(Duration::from_millis(10)).await;
984
985 let _ = marshaled.verify(context_a, commitment_a).await.await;
987
988 let _ = marshaled.verify(context_b, commitment_b).await.await;
990
991 let certify_b = marshaled.certify(round_b, commitment_b).await;
993 assert!(
994 certify_b.await.unwrap(),
995 "Block B certification should succeed"
996 );
997
998 let certify_a = marshaled.certify(round_a, commitment_a).await;
1000
1001 select! {
1002 result = certify_a => {
1003 assert!(result.unwrap(), "Block A certification should succeed");
1004 },
1005 _ = context.sleep(Duration::from_secs(5)) => {
1006 panic!("Block A certification timed out");
1007 },
1008 }
1009 })
1010 }
1011
1012 #[test_traced("WARN")]
1013 fn test_marshaled_rejects_unsupported_epoch() {
1014 #[derive(Clone)]
1015 struct LimitedEpocher {
1016 inner: FixedEpocher,
1017 max_epoch: u64,
1018 }
1019
1020 impl Epocher for LimitedEpocher {
1021 fn containing(&self, height: Height) -> Option<crate::types::EpochInfo> {
1022 let bounds = self.inner.containing(height)?;
1023 if bounds.epoch().get() > self.max_epoch {
1024 None
1025 } else {
1026 Some(bounds)
1027 }
1028 }
1029
1030 fn first(&self, epoch: Epoch) -> Option<Height> {
1031 if epoch.get() > self.max_epoch {
1032 None
1033 } else {
1034 self.inner.first(epoch)
1035 }
1036 }
1037
1038 fn last(&self, epoch: Epoch) -> Option<Height> {
1039 if epoch.get() > self.max_epoch {
1040 None
1041 } else {
1042 self.inner.last(epoch)
1043 }
1044 }
1045 }
1046
1047 let runner = deterministic::Runner::timed(Duration::from_secs(60));
1048 runner.start(|mut context| async move {
1049 let Fixture {
1050 participants,
1051 schemes,
1052 ..
1053 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1054 let mut oracle = setup_network_with_participants(
1055 context.child("network"),
1056 NZUsize!(1),
1057 participants.clone(),
1058 1024 * 1024,
1059 )
1060 .await;
1061
1062 let me = participants[0].clone();
1063
1064 let setup = StandardHarness::setup_validator(
1065 context.child("validator").with_attribute("index", 0),
1066 &mut oracle,
1067 me.clone(),
1068 ConstantProvider::new(schemes[0].clone()),
1069 )
1070 .await;
1071 let marshal = setup.mailbox;
1072
1073 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1074 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1075 let limited_epocher = LimitedEpocher {
1076 inner: FixedEpocher::new(BLOCKS_PER_EPOCH),
1077 max_epoch: 0,
1078 };
1079
1080 let mut marshaled = Deferred::new(
1081 context.child("deferred"),
1082 mock_app,
1083 marshal.clone(),
1084 limited_epocher,
1085 );
1086
1087 let parent_ctx = Ctx {
1089 round: Round::new(Epoch::zero(), View::new(19)),
1090 leader: default_leader(),
1091 parent: (View::zero(), genesis.digest()),
1092 };
1093 let parent =
1094 B::new::<Sha256>(parent_ctx.clone(), genesis.digest(), Height::new(19), 1000);
1095 let parent_digest = parent.digest();
1096
1097 assert!(
1098 marshal
1099 .clone()
1100 .verified(
1101 Round::new(Epoch::zero(), View::new(19)),
1102 ready(parent.clone())
1103 )
1104 .await
1105 );
1106
1107 let unsupported_round = Round::new(Epoch::new(1), View::new(20));
1109 let unsupported_context = Ctx {
1110 round: unsupported_round,
1111 leader: me.clone(),
1112 parent: (View::new(19), parent_digest),
1113 };
1114 let block = B::new::<Sha256>(
1115 unsupported_context.clone(),
1116 parent_digest,
1117 Height::new(20),
1118 2000,
1119 );
1120 let block_commitment = block.digest();
1121
1122 assert!(
1123 marshal
1124 .clone()
1125 .verified(unsupported_round, ready(block.clone()))
1126 .await
1127 );
1128
1129 context.sleep(Duration::from_millis(10)).await;
1130
1131 let verify_result = marshaled
1134 .verify(unsupported_context, block_commitment)
1135 .await;
1136
1137 let optimistic_result = verify_result.await;
1139
1140 assert!(
1142 !optimistic_result.unwrap(),
1143 "Optimistic verify should reject block in unsupported epoch"
1144 );
1145 })
1146 }
1147
1148 #[test_traced("WARN")]
1154 fn test_marshaled_rejects_mismatched_context() {
1155 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1156 runner.start(|mut context| async move {
1157 let Fixture {
1158 participants,
1159 schemes,
1160 ..
1161 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1162 let mut oracle = setup_network_with_participants(
1163 context.child("network"),
1164 NZUsize!(1),
1165 participants.clone(),
1166 1024 * 1024,
1167 )
1168 .await;
1169
1170 let me = participants[0].clone();
1171
1172 let setup = StandardHarness::setup_validator(
1173 context.child("validator").with_attribute("index", 0),
1174 &mut oracle,
1175 me.clone(),
1176 ConstantProvider::new(schemes[0].clone()),
1177 )
1178 .await;
1179 let marshal = setup.mailbox;
1180
1181 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1182 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1183
1184 let mut marshaled = Deferred::new(
1185 context.child("deferred"),
1186 mock_app,
1187 marshal.clone(),
1188 FixedEpocher::new(BLOCKS_PER_EPOCH),
1189 );
1190
1191 let parent_ctx = Ctx {
1193 round: Round::new(Epoch::zero(), View::new(1)),
1194 leader: default_leader(),
1195 parent: (View::zero(), genesis.digest()),
1196 };
1197 let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
1198 let parent_commitment = parent.digest();
1199
1200 assert!(
1201 marshal
1202 .clone()
1203 .verified(
1204 Round::new(Epoch::zero(), View::new(1)),
1205 ready(parent.clone())
1206 )
1207 .await
1208 );
1209
1210 let round_a = Round::new(Epoch::zero(), View::new(2));
1212 let context_a = Ctx {
1213 round: round_a,
1214 leader: me.clone(),
1215 parent: (View::new(1), parent_commitment),
1216 };
1217 let block_a = B::new::<Sha256>(context_a, parent.digest(), Height::new(2), 200);
1218 let commitment_a = block_a.digest();
1219 assert!(marshal.verified(round_a, ready(block_a)).await);
1220
1221 context.sleep(Duration::from_millis(10)).await;
1222
1223 let round_b = Round::new(Epoch::zero(), View::new(3));
1225 let context_b = Ctx {
1226 round: round_b,
1227 leader: participants[1].clone(),
1228 parent: (View::new(1), parent_commitment),
1229 };
1230
1231 let verify_rx = marshaled.verify(context_b, commitment_a).await;
1232 select! {
1233 result = verify_rx => {
1234 assert!(
1235 !result.unwrap(),
1236 "mismatched context hash should be rejected"
1237 );
1238 },
1239 _ = context.sleep(Duration::from_secs(5)) => {
1240 panic!("verify should reject mismatched context hash promptly");
1241 },
1242 }
1243 })
1244 }
1245
1246 #[test_traced("WARN")]
1250 fn test_deferred_certify_recovers_after_verify_receiver_drop() {
1251 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1252 runner.start(|mut context| async move {
1253 let Fixture {
1254 participants,
1255 schemes,
1256 ..
1257 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1258 let mut oracle = setup_network_with_participants(
1259 context.child("network"),
1260 NZUsize!(1),
1261 participants.clone(),
1262 1024 * 1024,
1263 )
1264 .await;
1265
1266 let me = participants[0].clone();
1267 let setup = StandardHarness::setup_validator(
1268 context.child("validator").with_attribute("index", 0),
1269 &mut oracle,
1270 me.clone(),
1271 ConstantProvider::new(schemes[0].clone()),
1272 )
1273 .await;
1274 let marshal = setup.mailbox;
1275
1276 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1277 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1278 let mut marshaled = Deferred::new(
1279 context.child("deferred"),
1280 mock_app,
1281 marshal.clone(),
1282 FixedEpocher::new(BLOCKS_PER_EPOCH),
1283 );
1284
1285 let round = Round::new(Epoch::zero(), View::new(1));
1286 let block_context = Ctx {
1287 round,
1288 leader: me,
1289 parent: (View::zero(), genesis.digest()),
1290 };
1291 let block =
1292 B::new::<Sha256>(block_context.clone(), genesis.digest(), Height::new(1), 100);
1293 let digest = block.digest();
1294
1295 let verify_rx = marshaled.verify(block_context, digest).await;
1296 drop(verify_rx);
1297
1298 context.sleep(Duration::from_millis(10)).await;
1301
1302 assert!(marshal.verified(round, ready(block)).await);
1303 let certify_rx = marshaled.certify(round, digest).await;
1304 select! {
1305 result = certify_rx => {
1306 assert!(
1307 result.expect("certify result missing"),
1308 "certify should recover after verify receiver drop"
1309 );
1310 },
1311 _ = context.sleep(Duration::from_secs(5)) => {
1312 panic!("certify should recover promptly after verify drop");
1313 },
1314 }
1315 });
1316 }
1317
1318 #[test_traced("WARN")]
1325 fn test_deferred_store_overlaps_app_verify() {
1326 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1327 runner.start(|mut context| async move {
1328 let Fixture {
1329 participants,
1330 schemes,
1331 ..
1332 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1333 let mut oracle = setup_network_with_participants(
1334 context.child("network"),
1335 NZUsize!(1),
1336 participants.clone(),
1337 1024 * 1024,
1338 )
1339 .await;
1340
1341 let me = participants[0].clone();
1342
1343 let setup = StandardHarness::setup_validator(
1344 context.child("validator").with_attribute("index", 0),
1345 &mut oracle,
1346 me.clone(),
1347 ConstantProvider::new(schemes[0].clone()),
1348 )
1349 .await;
1350 let marshal = setup.mailbox;
1351 let buffer = setup.extra;
1352
1353 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1354 let (mock_app, verify_started, release_verify): (GatedVerifyingApp<B, S>, _, _) =
1355 GatedVerifyingApp::new();
1356 let mut marshaled = Deferred::new(
1357 context.child("deferred"),
1358 mock_app,
1359 marshal.clone(),
1360 FixedEpocher::new(BLOCKS_PER_EPOCH),
1361 );
1362
1363 let parent = make_raw_block(genesis.digest(), Height::new(1), 100);
1367 let parent_digest = parent.digest();
1368
1369 let child_round = Round::new(Epoch::zero(), View::new(2));
1370 let child_ctx = Ctx {
1371 round: child_round,
1372 leader: me,
1373 parent: (View::new(1), parent_digest),
1374 };
1375 let child = B::new::<Sha256>(child_ctx.clone(), parent_digest, Height::new(2), 200);
1376 let child_digest = child.digest();
1377
1378 assert!(
1379 buffer
1380 .broadcast(commonware_p2p::Recipients::Some(vec![]), ready(parent))
1381 .accepted(),
1382 "buffer broadcast for parent should be accepted"
1383 );
1384 assert!(
1385 buffer
1386 .broadcast(commonware_p2p::Recipients::Some(vec![]), ready(child))
1387 .accepted(),
1388 "buffer broadcast for child should be accepted"
1389 );
1390
1391 let optimistic_rx = marshaled.verify(child_ctx, child_digest).await;
1394 assert!(
1395 optimistic_rx
1396 .await
1397 .expect("optimistic verify should resolve"),
1398 "optimistic verify should accept the available block"
1399 );
1400
1401 verify_started
1405 .await
1406 .expect("verify should reach the gated application");
1407 assert!(
1408 marshal.get_block(&child_digest).await.is_some(),
1409 "the store request runs concurrently with app.verify, so the block is locally queryable while verification is still gated"
1410 );
1411
1412 release_verify.send_lossy(());
1414 let certify_rx = marshaled.certify(child_round, child_digest).await;
1415 select! {
1416 result = certify_rx => {
1417 assert!(
1418 result.expect("certify result missing"),
1419 "certify should succeed once verification passes"
1420 );
1421 },
1422 _ = context.sleep(Duration::from_secs(5)) => {
1423 panic!("certify should resolve after verification is released");
1424 },
1425 }
1426 });
1427 }
1428
1429 #[test_traced("WARN")]
1435 fn test_propose_reuses_reproposed_boundary_block_on_restart() {
1436 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1437 runner.start(|mut context| async move {
1438 let Fixture {
1439 participants,
1440 schemes,
1441 ..
1442 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1443 let mut oracle = setup_network_with_participants(
1444 context.child("network"),
1445 NZUsize!(1),
1446 participants.clone(),
1447 1024 * 1024,
1448 )
1449 .await;
1450
1451 let me = participants[0].clone();
1452 let setup = StandardHarness::setup_validator(
1453 context.child("validator").with_attribute("index", 0),
1454 &mut oracle,
1455 me.clone(),
1456 ConstantProvider::new(schemes[0].clone()),
1457 )
1458 .await;
1459 let marshal = setup.mailbox;
1460
1461 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1462
1463 let boundary_height = Height::new(BLOCKS_PER_EPOCH.get() - 1);
1466 let boundary_round = Round::new(Epoch::zero(), View::new(boundary_height.get()));
1467 let boundary_block = B::new::<Sha256>(
1468 Ctx {
1469 round: boundary_round,
1470 leader: default_leader(),
1471 parent: (View::zero(), genesis.digest()),
1472 },
1473 genesis.digest(),
1474 boundary_height,
1475 1900,
1476 );
1477 let boundary_digest = boundary_block.digest();
1478 let round = Round::new(Epoch::zero(), View::new(boundary_height.get() + 1));
1479 assert!(marshal.verified(round, ready(boundary_block)).await);
1480
1481 let ctx = Ctx {
1482 round,
1483 leader: me.clone(),
1484 parent: (View::new(boundary_height.get()), boundary_digest),
1485 };
1486
1487 let (mock_app, verify_started, _release_verify): (GatedVerifyingApp<B, S>, _, _) =
1491 GatedVerifyingApp::new();
1492 let mut marshaled = Deferred::new(
1493 context.child("deferred"),
1494 mock_app,
1495 marshal.clone(),
1496 FixedEpocher::new(BLOCKS_PER_EPOCH),
1497 );
1498
1499 let digest_rx = marshaled.propose(ctx).await;
1500 let digest = digest_rx.await.expect("propose must return a digest");
1501 assert_eq!(
1502 digest, boundary_digest,
1503 "propose must re-propose the boundary block marshal already persisted for this round"
1504 );
1505
1506 let _ = marshaled.broadcast(digest, Plan::Propose { round });
1507 let certify_rx = marshaled.certify(round, digest).await;
1508 select! {
1509 result = certify_rx => {
1510 assert!(
1511 result.expect("certify result missing"),
1512 "re-proposed boundary block must certify through the relay handshake"
1513 );
1514 },
1515 _ = verify_started => {
1516 panic!("certifying a re-proposed boundary block must not run app verification");
1517 },
1518 }
1519 });
1520 }
1521
1522 #[test_traced("WARN")]
1527 fn test_deferred_propose_then_certify_persists_block() {
1528 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1529 runner.start(|mut context| async move {
1530 let Fixture {
1531 participants,
1532 schemes,
1533 ..
1534 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1535 let mut oracle = setup_network_with_participants(
1536 context.child("network"),
1537 NZUsize!(1),
1538 participants.clone(),
1539 1024 * 1024,
1540 )
1541 .await;
1542
1543 let me = participants[0].clone();
1544 let setup = StandardHarness::setup_validator(
1545 context.child("validator").with_attribute("index", 0),
1546 &mut oracle,
1547 me.clone(),
1548 ConstantProvider::new(schemes[0].clone()),
1549 )
1550 .await;
1551 let marshal = setup.mailbox;
1552 let actor_handle = setup.actor_handle;
1553
1554 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1555
1556 let parent_round = Round::new(Epoch::zero(), View::new(1));
1558 let parent_ctx = Ctx {
1559 round: parent_round,
1560 leader: default_leader(),
1561 parent: (View::zero(), genesis.digest()),
1562 };
1563 let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
1564 let parent_digest = parent.digest();
1565 assert!(marshal.verified(parent_round, ready(parent)).await);
1566
1567 let round = Round::new(Epoch::zero(), View::new(2));
1569 let ctx = Ctx {
1570 round,
1571 leader: me.clone(),
1572 parent: (View::new(1), parent_digest),
1573 };
1574 let child = B::new::<Sha256>(ctx.clone(), parent_digest, Height::new(2), 200);
1575 let child_digest = child.digest();
1576 let mock_app: MockVerifyingApp<B, S> =
1577 MockVerifyingApp::new().with_propose_result(child);
1578 let mut marshaled = Deferred::new(
1579 context.child("deferred"),
1580 mock_app,
1581 marshal.clone(),
1582 FixedEpocher::new(BLOCKS_PER_EPOCH),
1583 );
1584
1585 let digest = marshaled
1586 .propose(ctx)
1587 .await
1588 .await
1589 .expect("propose must return a digest");
1590 assert_eq!(
1591 digest, child_digest,
1592 "propose must return the built block's digest"
1593 );
1594
1595 assert!(
1597 marshaled
1598 .certify(round, child_digest)
1599 .await
1600 .await
1601 .expect("certify result missing"),
1602 "certify must succeed for the leader's own proposal"
1603 );
1604
1605 actor_handle.abort();
1607 let _ = actor_handle.await;
1608 drop(marshaled);
1609 drop(marshal);
1610
1611 let setup2 = StandardHarness::setup_validator(
1612 context
1613 .child("validator_restart")
1614 .with_attribute("index", 0),
1615 &mut oracle,
1616 me,
1617 ConstantProvider::new(schemes[0].clone()),
1618 )
1619 .await;
1620 let marshal2 = setup2.mailbox;
1621
1622 assert!(
1623 marshal2.get_block(&child_digest).await.is_some(),
1624 "certify resolved true for the leader's own proposal so the block must be durable"
1625 );
1626 });
1627 }
1628
1629 struct EquivocationFixture {
1638 marshaled: Deferred<deterministic::Context, S, MockVerifyingApp<B, S>, B, FixedEpocher>,
1639 round: Round,
1640 digest: <B as Digestible>::Digest,
1641 embedded_ctx: Ctx,
1642 equivocating_ctx: Ctx,
1643 _extra: <StandardHarness as TestHarness>::ValidatorExtra,
1644 }
1645
1646 async fn equivocation_fixture(
1647 context: &mut deterministic::Context,
1648 app: MockVerifyingApp<B, S>,
1649 ) -> EquivocationFixture {
1650 let Fixture {
1651 participants,
1652 schemes,
1653 ..
1654 } = bls12381_threshold_vrf::fixture::<V, _>(context, NAMESPACE, NUM_VALIDATORS);
1655 let mut oracle = setup_network_with_participants(
1656 context.child("network"),
1657 NZUsize!(1),
1658 participants.clone(),
1659 1024 * 1024,
1660 )
1661 .await;
1662
1663 let me = participants[0].clone();
1664 let setup = StandardHarness::setup_validator(
1665 context.child("validator").with_attribute("index", 0),
1666 &mut oracle,
1667 me,
1668 ConstantProvider::new(schemes[0].clone()),
1669 )
1670 .await;
1671 let marshal = setup.mailbox;
1672 let buffer = setup.extra;
1673
1674 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1675 let leader = participants[1].clone();
1676
1677 let certified_round = Round::new(Epoch::zero(), View::new(1));
1679 let certified_ctx = Ctx {
1680 round: certified_round,
1681 leader: default_leader(),
1682 parent: (View::zero(), genesis.digest()),
1683 };
1684 let certified = B::new::<Sha256>(certified_ctx, genesis.digest(), Height::new(1), 100);
1685 let certified_digest = certified.digest();
1686 assert!(marshal.verified(certified_round, ready(certified)).await);
1687
1688 let notarized_round = Round::new(Epoch::zero(), View::new(2));
1691 let notarized_ctx = Ctx {
1692 round: notarized_round,
1693 leader: leader.clone(),
1694 parent: (View::new(1), certified_digest),
1695 };
1696 let notarized = B::new::<Sha256>(notarized_ctx, certified_digest, Height::new(2), 200);
1697 let notarized_digest = notarized.digest();
1698 assert!(marshal.verified(notarized_round, ready(notarized)).await);
1699
1700 let round = Round::new(Epoch::zero(), View::new(3));
1703 let embedded_ctx = Ctx {
1704 round,
1705 leader: leader.clone(),
1706 parent: (View::new(2), notarized_digest),
1707 };
1708 let block = B::new::<Sha256>(embedded_ctx.clone(), notarized_digest, Height::new(3), 300);
1709 let digest = block.digest();
1710 assert!(
1711 buffer
1712 .broadcast(commonware_p2p::Recipients::Some(vec![]), ready(block))
1713 .accepted(),
1714 "buffer broadcast for the candidate should be accepted"
1715 );
1716
1717 let equivocating_ctx = Ctx {
1718 round,
1719 leader,
1720 parent: (View::new(1), certified_digest),
1721 };
1722
1723 let marshaled = Deferred::new(
1724 context.child("deferred"),
1725 app,
1726 marshal,
1727 FixedEpocher::new(BLOCKS_PER_EPOCH),
1728 );
1729 context.sleep(Duration::from_millis(10)).await;
1730
1731 EquivocationFixture {
1732 marshaled,
1733 round,
1734 digest,
1735 embedded_ctx,
1736 equivocating_ctx,
1737 _extra: buffer,
1738 }
1739 }
1740
1741 #[test_traced("WARN")]
1757 fn test_certify_not_poisoned_by_equivocating_parent_verify() {
1758 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1759 runner.start(|mut context| async move {
1760 let mut fixture = equivocation_fixture(&mut context, MockVerifyingApp::new()).await;
1761
1762 let verify_rx = fixture
1763 .marshaled
1764 .verify(fixture.equivocating_ctx.clone(), fixture.digest)
1765 .await;
1766 assert!(
1767 !verify_rx.await.expect("verify result missing"),
1768 "the equivocating proposal must not be notarized"
1769 );
1770
1771 let certify_rx = fixture
1772 .marshaled
1773 .certify(fixture.round, fixture.digest)
1774 .await;
1775 select! {
1776 result = certify_rx => {
1777 assert!(
1778 result.expect("certify result missing"),
1779 "certify of the notarized digest must not adopt the verdict computed under the equivocating context"
1780 );
1781 },
1782 _ = context.sleep(Duration::from_secs(5)) => {
1783 panic!("certify should resolve promptly");
1784 },
1785 }
1786 });
1787 }
1788
1789 #[test_traced("WARN")]
1794 fn test_certify_honors_application_rejection() {
1795 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1796 runner.start(|mut context| async move {
1797 let mut fixture =
1798 equivocation_fixture(&mut context, MockVerifyingApp::with_verify_result(false))
1799 .await;
1800
1801 let verify_rx = fixture
1802 .marshaled
1803 .verify(fixture.embedded_ctx.clone(), fixture.digest)
1804 .await;
1805 assert!(
1806 verify_rx.await.expect("verify result missing"),
1807 "optimistic verify accepts an available block with a matching context"
1808 );
1809
1810 let certify_rx = fixture
1811 .marshaled
1812 .certify(fixture.round, fixture.digest)
1813 .await;
1814 select! {
1815 result = certify_rx => {
1816 assert!(
1817 !result.expect("certify result missing"),
1818 "certify must propagate the application rejection"
1819 );
1820 },
1821 _ = context.sleep(Duration::from_secs(5)) => {
1822 panic!("certify should resolve promptly");
1823 },
1824 }
1825 });
1826 }
1827
1828 #[test_traced("WARN")]
1832 fn test_certify_without_prior_verify_honors_application_rejection() {
1833 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1834 runner.start(|mut context| async move {
1835 let mut fixture =
1836 equivocation_fixture(&mut context, MockVerifyingApp::with_verify_result(false))
1837 .await;
1838
1839 let certify_rx = fixture
1842 .marshaled
1843 .certify(fixture.round, fixture.digest)
1844 .await;
1845 select! {
1846 result = certify_rx => {
1847 assert!(
1848 !result.expect("certify result missing"),
1849 "certify must propagate the application rejection"
1850 );
1851 },
1852 _ = context.sleep(Duration::from_secs(5)) => {
1853 panic!("certify should resolve promptly");
1854 },
1855 }
1856 });
1857 }
1858
1859 impl Deferred<deterministic::Context, S, MockVerifyingApp<B, S>, B, FixedEpocher> {
1860 pub(in crate::marshal::standard) const fn gates(&self) -> &Gates<D, Digested<B>> {
1863 &self.gates
1864 }
1865 }
1866}