1use crate::{
54 Application, Automaton, Block, CertifiableAutomaton, Epochable, Handoff, Heightable, Relay,
55 Reporter,
56 marshal::{
57 ancestry::Ancestry,
58 application::{
59 build::{self, Resolved},
60 gates::{GateOutcome, Gates},
61 relay,
62 },
63 core::{CommitmentFallback, DigestFallback, Mailbox},
64 standard::{
65 Standard,
66 validation::{
67 Decision, ParentCheck, await_and_validate_parent, precheck_epoch_and_reproposal,
68 run_app_verify,
69 },
70 },
71 },
72 simplex::{
73 Plan,
74 types::{Activity, Context},
75 },
76 types::{Epocher, Round, View},
77};
78use commonware_actor::Feedback;
79use commonware_codec::Codec;
80use commonware_cryptography::{Digested, Digestible, certificate::Scheme};
81use commonware_macros::select;
82use commonware_runtime::{
83 Clock, Metrics, Spawner,
84 telemetry::{
85 metrics::{
86 MetricsExt as _,
87 histogram::{Buckets, Timed},
88 },
89 traces::TracedExt as _,
90 },
91};
92use commonware_utils::channel::{fallible::OneshotExt, oneshot};
93use std::{
94 future::{Future, ready},
95 sync::Arc,
96};
97use tracing::{Instrument as _, debug, info_span};
98
99async fn await_block_subscription<T, D>(
101 tx: &mut oneshot::Sender<bool>,
102 block_rx: oneshot::Receiver<T>,
103 digest: &D,
104 stage: &'static str,
105) -> Option<T>
106where
107 D: std::fmt::Debug,
108{
109 select! {
110 _ = tx.closed() => {
111 debug!(
112 stage,
113 reason = "consensus dropped receiver",
114 "skipping block wait"
115 );
116 None
117 },
118 result = block_rx => {
119 if result.is_err() {
120 debug!(
121 stage,
122 ?digest,
123 reason = "failed to fetch block",
124 "skipping block wait"
125 );
126 }
127 result.ok()
128 },
129 }
130}
131
132pub struct Inline<E, S, A, B, ES>
150where
151 S: Scheme,
152 A: Application<E>,
153 B: Block + Codec,
154 ES: Epocher,
155{
156 context: Arc<E>,
157 application: A,
158 marshal: Mailbox<S, Standard<B>>,
159 epocher: ES,
160 gates: Gates<B::Digest, Digested<B>>,
161
162 build_duration: Timed,
163 proposal_parent_fetch_duration: Timed,
164 ancestor_fetch_duration: Timed,
165}
166
167impl<E, S, A, B, ES> Clone for Inline<E, S, A, B, ES>
168where
169 S: Scheme,
170 A: Application<E>,
171 B: Block + Codec,
172 ES: Epocher,
173{
174 fn clone(&self) -> Self {
175 Self {
176 context: self.context.clone(),
177 application: self.application.clone(),
178 marshal: self.marshal.clone(),
179 epocher: self.epocher.clone(),
180 gates: self.gates.clone(),
181 build_duration: self.build_duration.clone(),
182 proposal_parent_fetch_duration: self.proposal_parent_fetch_duration.clone(),
183 ancestor_fetch_duration: self.ancestor_fetch_duration.clone(),
184 }
185 }
186}
187
188impl<E, S, A, B, ES> Inline<E, S, A, B, ES>
189where
190 E: Metrics + Clock,
191 S: Scheme,
192 A: Application<
193 E,
194 Block = B,
195 SigningScheme = S,
196 Context = Context<B::Digest, S::PublicKey>,
197 Input = (),
198 >,
199 B: Block + Codec,
200 ES: Epocher,
201{
202 pub fn new(context: E, application: A, marshal: Mailbox<S, Standard<B>>, epocher: ES) -> Self {
206 let build_histogram = context.histogram(
207 "build_duration",
208 "Histogram of time taken for the application to build a new block, in seconds",
209 Buckets::LOCAL,
210 );
211 let build_duration = Timed::new(build_histogram);
212 let parent_fetch_histogram = context.histogram(
213 "parent_fetch_duration",
214 "Histogram of time taken to fetch a parent block in proposal, in seconds",
215 Buckets::LOCAL,
216 );
217 let proposal_parent_fetch_duration = Timed::new(parent_fetch_histogram);
218 let ancestor_fetch_histogram = context.histogram(
219 "ancestor_fetch_duration",
220 "Histogram of time taken to fetch a block via the ancestry stream, in seconds",
221 Buckets::LOCAL,
222 );
223 let ancestor_fetch_duration = Timed::new(ancestor_fetch_histogram);
224
225 Self {
226 context: Arc::new(context),
227 application,
228 marshal,
229 epocher,
230 gates: Gates::new(),
231 build_duration,
232 proposal_parent_fetch_duration,
233 ancestor_fetch_duration,
234 }
235 }
236
237 fn checks(
244 &self,
245 consensus_context: Context<B::Digest, S::PublicKey>,
246 ) -> impl Future<Output = Resolved<B::Digest, Digested<B>, impl Ancestry<B>>> + Send + 'static
247 {
248 let context = self.context.clone();
249 let marshal = self.marshal.clone();
250 let epocher = self.epocher.clone();
251 let proposal_parent_fetch_duration = self.proposal_parent_fetch_duration.clone();
252 let ancestor_fetch_duration = self.ancestor_fetch_duration.clone();
253 let build_duration = self.build_duration.clone();
254 async move {
255 let (parent_view, parent_commitment) = consensus_context.parent;
264 let parent_request = marshal.subscribe_by_commitment(
265 parent_commitment,
266 CommitmentFallback::FetchByRound {
267 round: Round::new(consensus_context.epoch(), parent_view),
268 },
269 );
270 let parent_timer = proposal_parent_fetch_duration.timer(&*context);
271 let Ok(parent) = parent_request.await else {
272 debug!(
273 ?parent_commitment,
274 reason = "failed to fetch parent block",
275 "skipping proposal"
276 );
277 return Resolved::Skip;
278 };
279 parent_timer.observe(&*context);
280
281 let last_in_epoch = epocher
283 .last(consensus_context.epoch())
284 .expect("current epoch should exist");
285 if parent.height() == last_in_epoch {
286 return Resolved::Reuse(parent.digest(), parent);
287 }
288
289 let ancestor_stream =
290 marshal.ancestor_stream(context.clone(), [parent], ancestor_fetch_duration);
291 Resolved::Build(ancestor_stream, build_duration.timer(&*context), ())
292 }
293 }
294}
295
296impl<E, S, A, B, ES> Automaton for Inline<E, S, A, B, ES>
297where
298 E: Spawner + Metrics + Clock,
299 S: Scheme,
300 A: Application<
301 E,
302 Block = B,
303 SigningScheme = S,
304 Context = Context<B::Digest, S::PublicKey>,
305 Input = (),
306 >,
307 B: Block + Codec,
308 ES: Epocher,
309{
310 type Digest = B::Digest;
311 type Context = Context<Self::Digest, S::PublicKey>;
312
313 #[allow(clippy::async_yields_async)]
322 #[tracing::instrument(name = "marshal.inline.propose", level = "info", skip_all, fields(round = %consensus_context.round))]
323 async fn propose(
324 &mut self,
325 consensus_context: Context<Self::Digest, S::PublicKey>,
326 ) -> oneshot::Receiver<Self::Digest> {
327 let checks = self.checks(consensus_context.clone());
328 let span = info_span!(
329 "marshal.inline.propose.task",
330 round = %consensus_context.round
331 );
332 let round = consensus_context.round;
333 let (parent_view, parent_commitment) = consensus_context.parent;
334 build::propose(
335 self.context.as_ref(),
336 &self.application,
337 self.gates.clone(),
338 consensus_context,
339 checks,
340 span,
341 move || {
342 info_span!(
343 "marshal.inline.application.propose",
344 round = %round,
345 parent_view = parent_view.traced(),
346 parent = %parent_commitment
347 )
348 },
349 |block, ()| ready((block.digest(), block)),
350 )
351 }
352
353 #[allow(clippy::async_yields_async)]
366 #[tracing::instrument(name = "marshal.inline.verify", level = "info", skip_all, fields(round = %context.round, digest = %digest))]
367 async fn verify(
368 &mut self,
369 context: Context<Self::Digest, S::PublicKey>,
370 digest: Self::Digest,
371 ) -> oneshot::Receiver<bool> {
372 let round = context.round;
373
374 let block_request = self
380 .marshal
381 .subscribe_by_digest(digest, DigestFallback::Wait);
382 let (durable_tx, durable_rx) = oneshot::channel();
383 self.gates.insert(round, digest, durable_rx);
384
385 let marshal = self.marshal.clone();
386 let mut application = self.application.clone();
387 let epocher = self.epocher.clone();
388 let ancestor_fetch_duration = self.ancestor_fetch_duration.clone();
389
390 let (mut tx, rx) = oneshot::channel();
391 let runtime_context = self
392 .context
393 .child("inline_verify")
394 .with_attribute("round", round);
395 let span = info_span!(
396 "marshal.inline.verify.task",
397 round = %round,
398 digest = %digest
399 );
400 runtime_context.spawn(move |runtime_context| {
401 async move {
402 let parent_request = (digest != context.parent.1).then(|| {
409 let (parent_view, parent_commitment) = context.parent;
410 marshal.subscribe_by_commitment(
411 parent_commitment,
412 CommitmentFallback::FetchByRound {
413 round: Round::new(context.epoch(), parent_view),
414 },
415 )
416 });
417
418 let Some(block) =
419 await_block_subscription(&mut tx, block_request, &digest, "verification").await
420 else {
421 return;
422 };
423
424 let Some(decision) =
433 precheck_epoch_and_reproposal(&epocher, &marshal, &context, digest, block)
434 .await
435 else {
436 return;
437 };
438 let block = match decision {
439 Decision::Complete(valid) => {
440 tx.send_lossy(valid);
448 durable_tx.send_lossy(if valid {
449 GateOutcome::Ready(true)
450 } else {
451 GateOutcome::Recover
452 });
453 return;
454 }
455 Decision::Continue(block) => block,
456 };
457
458 let parent_request =
461 parent_request.expect("non-reproposal has a parent subscription");
462
463 let store = async {
477 if marshal.verified(round, block.clone()).await {
478 durable_tx.send_lossy(GateOutcome::Ready(true));
479 }
480 };
481 let verify_then_vote = async {
482 let parent = match await_and_validate_parent(
485 context.parent.1,
486 &block,
487 parent_request,
488 &mut tx,
489 )
490 .await
491 {
492 Some(ParentCheck::Valid(parent)) => parent,
493 Some(ParentCheck::Invalid) => {
494 tx.send_lossy(false);
495 return Some(false);
496 }
497 None => return None,
498 };
499 let valid = run_app_verify(
500 runtime_context,
501 context,
502 block.clone(),
503 parent,
504 &mut application,
505 &marshal,
506 &mut tx,
507 ancestor_fetch_duration,
508 )
509 .await;
510 if let Some(valid) = valid {
511 tx.send_lossy(valid);
512 }
513 valid
514 };
515 futures::join!(verify_then_vote, store);
516 }
517 .instrument(span)
518 });
519 rx
520 }
521}
522
523impl<E, S, A, B, ES> CertifiableAutomaton for Inline<E, S, A, B, ES>
527where
528 E: Spawner + Metrics + Clock,
529 S: Scheme,
530 A: Application<
531 E,
532 Block = B,
533 SigningScheme = S,
534 Context = Context<B::Digest, S::PublicKey>,
535 Input = (),
536 >,
537 B: Block + Codec,
538 ES: Epocher,
539{
540 #[allow(clippy::async_yields_async)]
546 #[tracing::instrument(name = "marshal.inline.prepare", level = "info", skip_all, fields(round = %consensus_context.round))]
547 async fn prepare(
548 &mut self,
549 consensus_context: Context<Self::Digest, S::PublicKey>,
550 ) -> oneshot::Receiver<Handoff<Self::Digest>> {
551 let checks = self.checks(consensus_context.clone());
552 let span = info_span!(
553 "marshal.inline.prepare.task",
554 round = %consensus_context.round
555 );
556 let round = consensus_context.round;
557 let (parent_view, parent_commitment) = consensus_context.parent;
558 build::prepare(
559 self.context.as_ref(),
560 &self.application,
561 self.gates.clone(),
562 consensus_context,
563 checks,
564 span,
565 move || {
566 info_span!(
567 "marshal.inline.application.prepare",
568 round = %round,
569 parent_view = parent_view.traced(),
570 parent = %parent_commitment
571 )
572 },
573 |block, ()| ready((block.digest(), block)),
574 )
575 }
576
577 #[allow(clippy::async_yields_async)]
578 #[tracing::instrument(name = "marshal.inline.certify", level = "info", skip_all, fields(round = %round, digest = %digest))]
579 async fn certify(&mut self, round: Round, digest: Self::Digest) -> oneshot::Receiver<bool> {
580 let task = self.gates.claim(round, digest, |block, ack| {
587 self.marshal.verified_deferred(round, block, ack)
588 });
589
590 if task.is_some() {
595 self.marshal.hint_notarized(round, digest);
596 }
597 let marshal = self.marshal.clone();
598 let (mut tx, rx) = oneshot::channel();
599 let context = self
600 .context
601 .child("inline_certify")
602 .with_attribute("round", round);
603 context.spawn(move |_| {
604 async move {
605 if let Some(task) = task {
608 let result = select! {
609 _ = tx.closed() => {
610 debug!(
611 reason = "consensus dropped receiver",
612 "skipping certification"
613 );
614 return;
615 },
616 result = task => result,
617 };
618 match result {
619 Ok(GateOutcome::Ready(verdict)) => {
620 tx.send_lossy(verdict);
621 return;
622 }
623 Ok(GateOutcome::Recover) | Err(_) => {}
624 }
625 }
626
627 let block_rx =
633 marshal.subscribe_by_digest(digest, DigestFallback::FetchByRound { round });
634 let Some(block) =
635 await_block_subscription(&mut tx, block_rx, &digest, "certification").await
636 else {
637 return;
638 };
639 if !marshal.certified(round, block).await {
640 return;
641 }
642 tx.send_lossy(true);
643 }
644 .instrument(info_span!(
645 "marshal.inline.certify.task",
646 round = %round,
647 digest = %digest
648 ))
649 });
650
651 rx
652 }
653}
654
655impl<E, S, A, B, ES> Relay for Inline<E, S, A, B, ES>
656where
657 E: Send + Sync + 'static,
658 S: Scheme,
659 A: Application<E, Block = B, Context = Context<B::Digest, S::PublicKey>>,
660 B: Block + Codec,
661 ES: Epocher,
662{
663 type Digest = B::Digest;
664 type PublicKey = S::PublicKey;
665 type Plan = Plan<S::PublicKey>;
666
667 fn broadcast(&mut self, commitment: Self::Digest, plan: Plan<S::PublicKey>) -> Feedback {
668 relay::broadcast(&self.gates, &self.marshal, commitment, plan)
669 }
670}
671
672impl<E, S, A, B, ES> Reporter for Inline<E, S, A, B, ES>
680where
681 E: Send + Sync + 'static,
682 S: Scheme,
683 A: Application<E, Block = B, Context = Context<B::Digest, S::PublicKey>>,
684 B: Block + Codec,
685 ES: Epocher,
686{
687 type Activity = Activity<S, B::Digest>;
688
689 fn report(&mut self, activity: Self::Activity) -> Feedback {
690 let retired = match &activity {
691 Activity::Finalization(finalization) => finalization.round(),
692 activity => Round::new(activity.epoch(), View::zero()),
693 };
694 self.gates.retain_after(retired);
695 self.marshal.report(activity)
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::Inline;
702 use crate::{
703 Application, Automaton, Block, CertifiableAutomaton, Relay,
704 marshal::{
705 application::gates::Gates,
706 mocks::{
707 harness::{
708 B, BLOCKS_PER_EPOCH, Ctx, D, NAMESPACE, NUM_VALIDATORS, S, StandardHarness,
709 TestHarness, V, default_leader, make_raw_block,
710 setup_network_with_participants,
711 },
712 verifying::{GatedVerifyingApp, MockVerifyingApp},
713 },
714 standard::tests::ready,
715 },
716 simplex::{scheme::bls12381_threshold::vrf as bls12381_threshold_vrf, types::Context},
717 types::{Epoch, FixedEpocher, Height, Round, View},
718 };
719 use commonware_broadcast::Broadcaster;
720 use commonware_codec::Codec;
721 use commonware_cryptography::{
722 Digested, Digestible, Hasher as _,
723 certificate::{ConstantProvider, Scheme, mocks::Fixture},
724 sha256::Sha256,
725 };
726 use commonware_macros::{select, test_traced};
727 use commonware_runtime::{Clock, Metrics, Runner, Spawner, Supervisor as _, deterministic};
728 use commonware_utils::{NZUsize, channel::fallible::OneshotExt};
729 use std::time::Duration;
730
731 #[allow(dead_code)]
733 fn assert_non_certifiable_block_supported<E, S, A, B, ES>()
734 where
735 E: Spawner + Metrics + Clock,
736 S: Scheme,
737 A: Application<
738 E,
739 Block = B,
740 SigningScheme = S,
741 Context = Context<B::Digest, S::PublicKey>,
742 Input = (),
743 >,
744 B: Block + Codec,
745 ES: crate::types::Epocher,
746 {
747 fn assert_automaton<T: Automaton>() {}
748 fn assert_certifiable<T: CertifiableAutomaton>() {}
749 fn assert_relay<T: Relay>() {}
750
751 assert_automaton::<Inline<E, S, A, B, ES>>();
752 assert_certifiable::<Inline<E, S, A, B, ES>>();
753 assert_relay::<Inline<E, S, A, B, ES>>();
754 }
755
756 #[test_traced("INFO")]
757 fn test_certify_returns_immediately_after_verify_fetches_block() {
758 let runner = deterministic::Runner::timed(Duration::from_secs(30));
759 runner.start(|mut context| async move {
760 let Fixture {
761 participants,
762 schemes,
763 ..
764 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
765 let mut oracle = setup_network_with_participants(
766 context.child("network"),
767 NZUsize!(1),
768 participants.clone(),
769 1024 * 1024,
770 )
771 .await;
772
773 let me = participants[0].clone();
774 let setup = StandardHarness::setup_validator(
775 context.child("validator").with_attribute("index", 0),
776 &mut oracle,
777 me.clone(),
778 ConstantProvider::new(schemes[0].clone()),
779 )
780 .await;
781 let marshal = setup.mailbox;
782
783 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
784 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
785 let mut inline = Inline::new(
786 context.child("inline"),
787 mock_app,
788 marshal.clone(),
789 FixedEpocher::new(BLOCKS_PER_EPOCH),
790 );
791
792 let parent_round = Round::new(Epoch::zero(), View::new(1));
794 let parent_ctx = Ctx {
795 round: parent_round,
796 leader: default_leader(),
797 parent: (View::zero(), genesis.digest()),
798 };
799 let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
800 let parent_digest = parent.digest();
801 assert!(marshal.verified(parent_round, ready(parent)).await);
802
803 let round = Round::new(Epoch::zero(), View::new(2));
804 let verify_context = Ctx {
805 round,
806 leader: me,
807 parent: (View::new(1), parent_digest),
808 };
809 let block =
810 B::new::<Sha256>(verify_context.clone(), parent_digest, Height::new(2), 200);
811 let digest = block.digest();
812 assert!(marshal.verified(round, ready(block)).await);
813
814 let verify_rx = inline.verify(verify_context, digest).await;
816 assert!(
817 verify_rx.await.unwrap(),
818 "verify should complete successfully before certify"
819 );
820
821 let certify_rx = inline.certify(round, digest).await;
823
824 select! {
825 result = certify_rx => {
826 assert!(
827 result.unwrap(),
828 "certify should return immediately once verify has fetched the block"
829 );
830 },
831 _ = context.sleep(Duration::from_secs(5)) => {
832 panic!("certify should not hang after local verify completed");
833 },
834 }
835 });
836 }
837
838 #[test_traced("INFO")]
839 fn test_certify_succeeds_without_verify_task() {
840 let runner = deterministic::Runner::timed(Duration::from_secs(30));
841 runner.start(|mut context| async move {
842 let Fixture {
843 participants,
844 schemes,
845 ..
846 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
847 let mut oracle = setup_network_with_participants(
848 context.child("network"),
849 NZUsize!(1),
850 participants.clone(),
851 1024 * 1024,
852 )
853 .await;
854
855 let me = participants[0].clone();
856 let setup = StandardHarness::setup_validator(
857 context.child("validator").with_attribute("index", 0),
858 &mut oracle,
859 me.clone(),
860 ConstantProvider::new(schemes[0].clone()),
861 )
862 .await;
863 let marshal = setup.mailbox;
864
865 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
866 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
867 let mut inline = Inline::new(
868 context.child("inline"),
869 mock_app,
870 marshal.clone(),
871 FixedEpocher::new(BLOCKS_PER_EPOCH),
872 );
873
874 let parent_round = Round::new(Epoch::zero(), View::new(1));
876 let parent_ctx = Ctx {
877 round: parent_round,
878 leader: default_leader(),
879 parent: (View::zero(), genesis.digest()),
880 };
881 let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
882 let parent_digest = parent.digest();
883 assert!(marshal.verified(parent_round, ready(parent)).await);
884
885 let round = Round::new(Epoch::zero(), View::new(2));
886 let verify_context = Ctx {
887 round,
888 leader: me,
889 parent: (View::new(1), parent_digest),
890 };
891 let block =
892 B::new::<Sha256>(verify_context.clone(), parent_digest, Height::new(2), 200);
893 let digest = block.digest();
894 assert!(marshal.verified(round, ready(block)).await);
895
896 let certify_rx = inline.certify(round, digest).await;
898
899 select! {
900 result = certify_rx => {
901 assert!(
902 result.unwrap(),
903 "certify should resolve once block availability is known"
904 );
905 },
906 _ = context.sleep(Duration::from_secs(5)) => {
907 panic!("certify should not hang when block is already available in marshal");
908 },
909 }
910 });
911 }
912
913 #[test_traced("INFO")]
914 fn test_certify_reproposal_uses_available_blocks_after_verify() {
915 let runner = deterministic::Runner::timed(Duration::from_secs(30));
916 runner.start(|mut context| async move {
917 let Fixture {
918 participants,
919 schemes,
920 ..
921 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
922 let mut oracle = setup_network_with_participants(
923 context.child("network"),
924 NZUsize!(1),
925 participants.clone(),
926 1024 * 1024,
927 )
928 .await;
929
930 let me = participants[0].clone();
931 let setup = StandardHarness::setup_validator(
932 context.child("validator").with_attribute("index", 0),
933 &mut oracle,
934 me.clone(),
935 ConstantProvider::new(schemes[0].clone()),
936 )
937 .await;
938 let marshal = setup.mailbox;
939 let marshal_actor_handle = setup.actor_handle;
940
941 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
942 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
943 let mut inline = Inline::new(context.child("inline"),
944 mock_app,
945 marshal.clone(),
946 FixedEpocher::new(BLOCKS_PER_EPOCH),
947 );
948
949 let boundary_height = Height::new(BLOCKS_PER_EPOCH.get() - 1);
950 let boundary_round = Round::new(Epoch::zero(), View::new(boundary_height.get()));
951 let boundary_block = B::new::<Sha256>(
952 Ctx {
953 round: boundary_round,
954 leader: default_leader(),
955 parent: (View::zero(), genesis.digest()),
956 },
957 genesis.digest(),
958 boundary_height,
959 1900,
960 );
961 let boundary_digest = boundary_block.digest();
962 assert!(
963 marshal.verified(boundary_round, ready(boundary_block)).await
964 );
965
966 let reproposal_round = Round::new(Epoch::zero(), View::new(boundary_height.get() + 1));
967 let reproposal_context = Ctx {
968 round: reproposal_round,
969 leader: me,
970 parent: (View::new(boundary_height.get()), boundary_digest),
971 };
972
973 let verify_rx = inline.verify(reproposal_context, boundary_digest).await;
974 assert!(
975 verify_rx.await.unwrap(),
976 "verify should accept a valid boundary re-proposal"
977 );
978
979 marshal_actor_handle.abort();
980 drop(marshal);
981 context.sleep(Duration::from_millis(1)).await;
982
983 let certify_rx = inline.certify(reproposal_round, boundary_digest).await;
984 select! {
985 result = certify_rx => {
986 assert!(
987 result.unwrap(),
988 "certify should use the available_blocks fast path for verified re-proposals"
989 );
990 },
991 _ = context.sleep(Duration::from_secs(5)) => {
992 panic!("certify should not depend on marshal after verify cached a re-proposal");
993 },
994 }
995 });
996 }
997
998 #[test_traced("WARN")]
1003 fn test_certify_recovers_from_invalid_reproposal_precheck() {
1004 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1005 runner.start(|mut context| async move {
1006 let Fixture {
1007 participants,
1008 schemes,
1009 ..
1010 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1011 let mut oracle = setup_network_with_participants(
1012 context.child("network"),
1013 NZUsize!(1),
1014 participants.clone(),
1015 1024 * 1024,
1016 )
1017 .await;
1018
1019 let me = participants[0].clone();
1020 let setup = StandardHarness::setup_validator(
1021 context.child("validator").with_attribute("index", 0),
1022 &mut oracle,
1023 me.clone(),
1024 ConstantProvider::new(schemes[0].clone()),
1025 )
1026 .await;
1027 let marshal = setup.mailbox;
1028
1029 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1030 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1031 let mut inline = Inline::new(
1032 context.child("inline"),
1033 mock_app,
1034 marshal.clone(),
1035 FixedEpocher::new(BLOCKS_PER_EPOCH),
1036 );
1037
1038 let block_round = Round::new(Epoch::zero(), View::new(2));
1040 let block = B::new::<Sha256>(
1041 Ctx {
1042 round: block_round,
1043 leader: default_leader(),
1044 parent: (View::zero(), genesis.digest()),
1045 },
1046 genesis.digest(),
1047 Height::new(2),
1048 200,
1049 );
1050 let digest = block.digest();
1051 assert!(marshal.verified(block_round, ready(block)).await);
1052
1053 let round = Round::new(Epoch::zero(), View::new(3));
1057 let reproposal_context = Ctx {
1058 round,
1059 leader: me,
1060 parent: (View::new(2), digest),
1061 };
1062 let verify_rx = inline.verify(reproposal_context, digest).await;
1063 assert!(
1064 !verify_rx.await.expect("verify result missing"),
1065 "a non-boundary re-proposal must be rejected"
1066 );
1067
1068 let certify_rx = inline.certify(round, digest).await;
1071 select! {
1072 result = certify_rx => {
1073 assert!(
1074 result.expect("certify result missing"),
1075 "certify must recover instead of adopting the precheck rejection"
1076 );
1077 },
1078 _ = context.sleep(Duration::from_secs(5)) => {
1079 panic!("certify should resolve promptly");
1080 },
1081 }
1082 });
1083 }
1084
1085 #[test_traced("WARN")]
1089 fn test_inline_certify_persists_block_before_resolving() {
1090 for seed in 0u64..16 {
1091 inline_certify_persists_block_before_resolving_at(seed);
1092 }
1093 }
1094
1095 fn inline_certify_persists_block_before_resolving_at(seed: u64) {
1096 let runner = deterministic::Runner::new(
1097 deterministic::Config::new()
1098 .with_seed(seed)
1099 .with_timeout(Some(Duration::from_secs(60))),
1100 );
1101 runner.start(|mut context| async move {
1102 let Fixture {
1103 participants,
1104 schemes,
1105 ..
1106 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1107 let mut oracle = setup_network_with_participants(
1108 context.child("network"),
1109 NZUsize!(1),
1110 participants.clone(),
1111 1024 * 1024,
1112 )
1113 .await;
1114
1115 let me = participants[0].clone();
1116
1117 let setup = StandardHarness::setup_validator(
1118 context.child("validator").with_attribute("index", 0),
1119 &mut oracle,
1120 me.clone(),
1121 ConstantProvider::new(schemes[0].clone()),
1122 )
1123 .await;
1124 let marshal = setup.mailbox;
1125 let buffer = setup.extra;
1126 let actor_handle = setup.actor_handle;
1127
1128 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1129 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1130 let mut inline = Inline::new(
1131 context.child("inline"),
1132 mock_app,
1133 marshal.clone(),
1134 FixedEpocher::new(BLOCKS_PER_EPOCH),
1135 );
1136
1137 let parent = make_raw_block(genesis.digest(), Height::new(1), 100);
1138 let parent_digest = parent.digest();
1139
1140 let child_round = Round::new(Epoch::zero(), View::new(2));
1141 let child_ctx = Ctx {
1142 round: child_round,
1143 leader: me.clone(),
1144 parent: (View::new(1), parent_digest),
1145 };
1146 let child = B::new::<Sha256>(child_ctx.clone(), parent_digest, Height::new(2), 200);
1147 let child_digest = child.digest();
1148
1149 assert!(
1150 buffer
1151 .broadcast(
1152 commonware_p2p::Recipients::Some(vec![]),
1153 ready(parent.clone())
1154 )
1155 .accepted(),
1156 "buffer broadcast for parent should be accepted"
1157 );
1158 assert!(
1159 buffer
1160 .broadcast(
1161 commonware_p2p::Recipients::Some(vec![]),
1162 ready(child.clone())
1163 )
1164 .accepted(),
1165 "buffer broadcast for child should be accepted"
1166 );
1167
1168 let verify_rx = inline.verify(child_ctx, child_digest).await;
1169 let certify_result = inline
1170 .certify(child_round, child_digest)
1171 .await
1172 .await
1173 .expect("certify result missing");
1174 assert!(certify_result, "certify should succeed");
1175
1176 actor_handle.abort();
1177 let _ = actor_handle.await;
1178 drop(verify_rx);
1179 drop(inline);
1180 drop(marshal);
1181 drop(buffer);
1182
1183 let setup2 = StandardHarness::setup_validator(
1184 context
1185 .child("validator_restart")
1186 .with_attribute("index", 0),
1187 &mut oracle,
1188 me.clone(),
1189 ConstantProvider::new(schemes[0].clone()),
1190 )
1191 .await;
1192 let marshal2 = setup2.mailbox;
1193
1194 let post_restart = marshal2.get_block(&child_digest).await;
1195 assert!(
1196 post_restart.is_some(),
1197 "certify resolved true so block must be durably persisted (seed={seed})"
1198 );
1199 });
1200 }
1201
1202 #[test_traced("WARN")]
1208 fn test_inline_propose_then_certify_persists_block() {
1209 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1210 runner.start(|mut context| async move {
1211 let Fixture {
1212 participants,
1213 schemes,
1214 ..
1215 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1216 let mut oracle = setup_network_with_participants(
1217 context.child("network"),
1218 NZUsize!(1),
1219 participants.clone(),
1220 1024 * 1024,
1221 )
1222 .await;
1223
1224 let me = participants[0].clone();
1225 let setup = StandardHarness::setup_validator(
1226 context.child("validator").with_attribute("index", 0),
1227 &mut oracle,
1228 me.clone(),
1229 ConstantProvider::new(schemes[0].clone()),
1230 )
1231 .await;
1232 let marshal = setup.mailbox;
1233 let actor_handle = setup.actor_handle;
1234
1235 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1236
1237 let parent_round = Round::new(Epoch::zero(), View::new(1));
1239 let parent_ctx = Ctx {
1240 round: parent_round,
1241 leader: default_leader(),
1242 parent: (View::zero(), genesis.digest()),
1243 };
1244 let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
1245 let parent_digest = parent.digest();
1246 assert!(marshal.verified(parent_round, ready(parent)).await);
1247
1248 let round = Round::new(Epoch::zero(), View::new(2));
1250 let ctx = Ctx {
1251 round,
1252 leader: me.clone(),
1253 parent: (View::new(1), parent_digest),
1254 };
1255 let child = B::new::<Sha256>(ctx.clone(), parent_digest, Height::new(2), 200);
1256 let child_digest = child.digest();
1257 let mock_app: MockVerifyingApp<B, S> =
1258 MockVerifyingApp::new().with_propose_result(child);
1259 let mut inline = Inline::new(
1260 context.child("inline"),
1261 mock_app,
1262 marshal.clone(),
1263 FixedEpocher::new(BLOCKS_PER_EPOCH),
1264 );
1265
1266 let digest = inline
1267 .propose(ctx)
1268 .await
1269 .await
1270 .expect("propose must return a digest");
1271 assert_eq!(
1272 digest, child_digest,
1273 "propose must return the built block's digest"
1274 );
1275
1276 assert!(
1278 inline
1279 .certify(round, child_digest)
1280 .await
1281 .await
1282 .expect("certify result missing"),
1283 "certify must succeed for the leader's own proposal"
1284 );
1285
1286 actor_handle.abort();
1288 let _ = actor_handle.await;
1289 drop(inline);
1290 drop(marshal);
1291
1292 let setup2 = StandardHarness::setup_validator(
1293 context
1294 .child("validator_restart")
1295 .with_attribute("index", 0),
1296 &mut oracle,
1297 me,
1298 ConstantProvider::new(schemes[0].clone()),
1299 )
1300 .await;
1301 let marshal2 = setup2.mailbox;
1302
1303 assert!(
1304 marshal2.get_block(&child_digest).await.is_some(),
1305 "certify resolved true for the leader's own proposal so the block must be durable"
1306 );
1307 });
1308 }
1309
1310 #[test_traced("WARN")]
1314 fn test_inline_certify_recovers_after_verify_receiver_drop() {
1315 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1316 runner.start(|mut context| async move {
1317 let Fixture {
1318 participants,
1319 schemes,
1320 ..
1321 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1322 let mut oracle = setup_network_with_participants(
1323 context.child("network"),
1324 NZUsize!(1),
1325 participants.clone(),
1326 1024 * 1024,
1327 )
1328 .await;
1329
1330 let me = participants[0].clone();
1331 let setup = StandardHarness::setup_validator(
1332 context.child("validator").with_attribute("index", 0),
1333 &mut oracle,
1334 me.clone(),
1335 ConstantProvider::new(schemes[0].clone()),
1336 )
1337 .await;
1338 let marshal = setup.mailbox;
1339
1340 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1341 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1342 let mut inline = Inline::new(
1343 context.child("inline"),
1344 mock_app,
1345 marshal.clone(),
1346 FixedEpocher::new(BLOCKS_PER_EPOCH),
1347 );
1348
1349 let round = Round::new(Epoch::zero(), View::new(1));
1350 let block_context = Ctx {
1351 round,
1352 leader: me,
1353 parent: (View::zero(), genesis.digest()),
1354 };
1355 let block =
1356 B::new::<Sha256>(block_context.clone(), genesis.digest(), Height::new(1), 100);
1357 let digest = block.digest();
1358
1359 let verify_rx = inline.verify(block_context, digest).await;
1360 drop(verify_rx);
1361
1362 context.sleep(Duration::from_millis(10)).await;
1365
1366 assert!(marshal.verified(round, ready(block)).await);
1367 let certify_rx = inline.certify(round, digest).await;
1368 select! {
1369 result = certify_rx => {
1370 assert!(
1371 result.expect("certify result missing"),
1372 "certify should recover after verify receiver drop"
1373 );
1374 },
1375 _ = context.sleep(Duration::from_secs(5)) => {
1376 panic!("certify should recover promptly after verify drop");
1377 },
1378 }
1379 });
1380 }
1381
1382 #[test_traced("WARN")]
1389 fn test_inline_store_overlaps_app_verify() {
1390 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1391 runner.start(|mut context| async move {
1392 let Fixture {
1393 participants,
1394 schemes,
1395 ..
1396 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1397 let mut oracle = setup_network_with_participants(
1398 context.child("network"),
1399 NZUsize!(1),
1400 participants.clone(),
1401 1024 * 1024,
1402 )
1403 .await;
1404
1405 let me = participants[0].clone();
1406
1407 let setup = StandardHarness::setup_validator(
1408 context.child("validator").with_attribute("index", 0),
1409 &mut oracle,
1410 me.clone(),
1411 ConstantProvider::new(schemes[0].clone()),
1412 )
1413 .await;
1414 let marshal = setup.mailbox;
1415 let buffer = setup.extra;
1416
1417 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1418 let (mock_app, verify_started, release_verify): (GatedVerifyingApp<B, S>, _, _) =
1419 GatedVerifyingApp::new();
1420 let mut inline = Inline::new(
1421 context.child("inline"),
1422 mock_app,
1423 marshal.clone(),
1424 FixedEpocher::new(BLOCKS_PER_EPOCH),
1425 );
1426
1427 let parent = make_raw_block(genesis.digest(), Height::new(1), 100);
1428 let parent_digest = parent.digest();
1429
1430 let child_round = Round::new(Epoch::zero(), View::new(2));
1431 let child_ctx = Ctx {
1432 round: child_round,
1433 leader: me.clone(),
1434 parent: (View::new(1), parent_digest),
1435 };
1436 let child = B::new::<Sha256>(child_ctx.clone(), parent_digest, Height::new(2), 200);
1437 let child_digest = child.digest();
1438
1439 assert!(
1440 buffer
1441 .broadcast(commonware_p2p::Recipients::Some(vec![]), ready(parent))
1442 .accepted(),
1443 "buffer broadcast for parent should be accepted"
1444 );
1445 assert!(
1446 buffer
1447 .broadcast(commonware_p2p::Recipients::Some(vec![]), ready(child))
1448 .accepted(),
1449 "buffer broadcast for child should be accepted"
1450 );
1451
1452 let verify_rx = inline.verify(child_ctx, child_digest).await;
1453
1454 verify_started
1458 .await
1459 .expect("verify should reach the gated application");
1460 assert!(
1461 marshal.get_block(&child_digest).await.is_some(),
1462 "the store request runs concurrently with app.verify, so the block is locally queryable while verification is still gated"
1463 );
1464
1465 release_verify.send_lossy(());
1468 assert!(
1469 verify_rx.await.expect("verify result missing"),
1470 "inline verify should pass once verification is released"
1471 );
1472 let certify_rx = inline.certify(child_round, child_digest).await;
1473 select! {
1474 result = certify_rx => {
1475 assert!(
1476 result.expect("certify result missing"),
1477 "certify should succeed once verification passes"
1478 );
1479 },
1480 _ = context.sleep(Duration::from_secs(5)) => {
1481 panic!("certify should resolve after verification is released");
1482 },
1483 }
1484 });
1485 }
1486
1487 #[test_traced("WARN")]
1494 fn test_certify_not_poisoned_by_equivocating_parent_verify() {
1495 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1496 runner.start(|mut context| async move {
1497 let Fixture {
1498 participants,
1499 schemes,
1500 ..
1501 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1502 let mut oracle = setup_network_with_participants(
1503 context.child("network"),
1504 NZUsize!(1),
1505 participants.clone(),
1506 1024 * 1024,
1507 )
1508 .await;
1509
1510 let me = participants[0].clone();
1511 let setup = StandardHarness::setup_validator(
1512 context.child("validator").with_attribute("index", 0),
1513 &mut oracle,
1514 me,
1515 ConstantProvider::new(schemes[0].clone()),
1516 )
1517 .await;
1518 let marshal = setup.mailbox;
1519 let buffer = setup.extra;
1520
1521 let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1522 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1523 let mut inline = Inline::new(
1524 context.child("inline"),
1525 mock_app,
1526 marshal.clone(),
1527 FixedEpocher::new(BLOCKS_PER_EPOCH),
1528 );
1529
1530 let leader = participants[1].clone();
1531
1532 let certified_round = Round::new(Epoch::zero(), View::new(1));
1534 let certified_ctx = Ctx {
1535 round: certified_round,
1536 leader: default_leader(),
1537 parent: (View::zero(), genesis.digest()),
1538 };
1539 let certified = B::new::<Sha256>(certified_ctx, genesis.digest(), Height::new(1), 100);
1540 let certified_digest = certified.digest();
1541 assert!(marshal.verified(certified_round, ready(certified)).await);
1542
1543 let notarized_round = Round::new(Epoch::zero(), View::new(2));
1546 let notarized_ctx = Ctx {
1547 round: notarized_round,
1548 leader: leader.clone(),
1549 parent: (View::new(1), certified_digest),
1550 };
1551 let notarized = B::new::<Sha256>(notarized_ctx, certified_digest, Height::new(2), 200);
1552 let notarized_digest = notarized.digest();
1553 assert!(marshal.verified(notarized_round, ready(notarized)).await);
1554
1555 let round = Round::new(Epoch::zero(), View::new(3));
1559 let block_ctx = Ctx {
1560 round,
1561 leader: leader.clone(),
1562 parent: (View::new(2), notarized_digest),
1563 };
1564 let block = B::new::<Sha256>(block_ctx, notarized_digest, Height::new(3), 300);
1565 let digest = block.digest();
1566 assert!(
1567 buffer
1568 .broadcast(commonware_p2p::Recipients::Some(vec![]), ready(block))
1569 .accepted(),
1570 "buffer broadcast for the candidate should be accepted"
1571 );
1572 context.sleep(Duration::from_millis(10)).await;
1573
1574 let equivocating_ctx = Ctx {
1579 round,
1580 leader,
1581 parent: (View::new(1), certified_digest),
1582 };
1583 let verify_rx = inline.verify(equivocating_ctx, digest).await;
1584 assert!(
1585 !verify_rx.await.expect("verify result missing"),
1586 "the equivocating proposal must not be notarized"
1587 );
1588
1589 let certify_rx = inline.certify(round, digest).await;
1591 select! {
1592 result = certify_rx => {
1593 assert!(
1594 result.expect("certify result missing"),
1595 "certify of the notarized digest must not adopt the verdict computed under the equivocating context"
1596 );
1597 },
1598 _ = context.sleep(Duration::from_secs(5)) => {
1599 panic!("certify should resolve promptly");
1600 },
1601 }
1602 });
1603 }
1604
1605 impl Inline<deterministic::Context, S, MockVerifyingApp<B, S>, B, FixedEpocher> {
1606 pub(in crate::marshal::standard) const fn gates(&self) -> &Gates<D, Digested<B>> {
1609 &self.gates
1610 }
1611 }
1612}