1use crate::{
46 marshal::{
47 application::gates::{self, Gates},
48 core::{CommitmentFallback, DigestFallback, Mailbox},
49 standard::{
50 relay,
51 validation::{
52 await_and_validate_parent, precheck_epoch_and_reproposal, run_app_verify, Decision,
53 ParentCheck,
54 },
55 Standard,
56 },
57 Update,
58 },
59 simplex::{types::Context, Plan},
60 types::{Epocher, Round},
61 Application, Automaton, Block, CertifiableAutomaton, Epochable, Relay, Reporter,
62};
63use commonware_actor::Feedback;
64use commonware_cryptography::certificate::Scheme;
65use commonware_macros::select;
66use commonware_runtime::{
67 telemetry::{
68 metrics::{
69 histogram::{Buckets, Timed},
70 MetricsExt as _,
71 },
72 traces::TracedExt as _,
73 },
74 Clock, Metrics, Spawner,
75};
76use commonware_utils::{
77 channel::{fallible::OneshotExt, oneshot},
78 sync::TracedAsyncMutex,
79};
80use rand_core::Rng;
81use std::sync::Arc;
82use tracing::{debug, info_span, Instrument as _};
83
84async fn await_block_subscription<T, D>(
86 tx: &mut oneshot::Sender<bool>,
87 block_rx: oneshot::Receiver<T>,
88 digest: &D,
89 stage: &'static str,
90) -> Option<T>
91where
92 D: std::fmt::Debug,
93{
94 select! {
95 _ = tx.closed() => {
96 debug!(
97 stage,
98 reason = "consensus dropped receiver",
99 "skipping block wait"
100 );
101 None
102 },
103 result = block_rx => {
104 if result.is_err() {
105 debug!(
106 stage,
107 ?digest,
108 reason = "failed to fetch block",
109 "skipping block wait"
110 );
111 }
112 result.ok()
113 },
114 }
115}
116
117pub struct Inline<E, S, A, B, ES>
134where
135 E: Rng + Spawner + Metrics + Clock,
136 S: Scheme,
137 A: Application<E>,
138 B: Block + Clone,
139 ES: Epocher,
140{
141 context: Arc<TracedAsyncMutex<E>>,
142 application: A,
143 marshal: Mailbox<S, Standard<B>>,
144 epocher: ES,
145 gates: Gates<B::Digest, B>,
146
147 build_duration: Timed,
148 proposal_parent_fetch_duration: Timed,
149 ancestor_fetch_duration: Timed,
150}
151
152impl<E, S, A, B, ES> Clone for Inline<E, S, A, B, ES>
153where
154 E: Rng + Spawner + Metrics + Clock,
155 S: Scheme,
156 A: Application<E>,
157 B: Block + Clone,
158 ES: Epocher,
159{
160 fn clone(&self) -> Self {
161 Self {
162 context: self.context.clone(),
163 application: self.application.clone(),
164 marshal: self.marshal.clone(),
165 epocher: self.epocher.clone(),
166 gates: self.gates.clone(),
167 build_duration: self.build_duration.clone(),
168 proposal_parent_fetch_duration: self.proposal_parent_fetch_duration.clone(),
169 ancestor_fetch_duration: self.ancestor_fetch_duration.clone(),
170 }
171 }
172}
173
174impl<E, S, A, B, ES> Inline<E, S, A, B, ES>
175where
176 E: Rng + Spawner + Metrics + Clock,
177 S: Scheme,
178 A: Application<E, Block = B, SigningScheme = S, Context = Context<B::Digest, S::PublicKey>>,
179 B: Block + Clone,
180 ES: Epocher,
181{
182 pub fn new(context: E, application: A, marshal: Mailbox<S, Standard<B>>, epocher: ES) -> Self {
186 let build_histogram = context.histogram(
187 "build_duration",
188 "Histogram of time taken for the application to build a new block, in seconds",
189 Buckets::LOCAL,
190 );
191 let build_duration = Timed::new(build_histogram);
192 let parent_fetch_histogram = context.histogram(
193 "parent_fetch_duration",
194 "Histogram of time taken to fetch a parent block in propose, in seconds",
195 Buckets::LOCAL,
196 );
197 let proposal_parent_fetch_duration = Timed::new(parent_fetch_histogram);
198 let ancestor_fetch_histogram = context.histogram(
199 "ancestor_fetch_duration",
200 "Histogram of time taken to fetch a block via the ancestry stream, in seconds",
201 Buckets::LOCAL,
202 );
203 let ancestor_fetch_duration = Timed::new(ancestor_fetch_histogram);
204
205 Self {
206 context: Arc::new(TracedAsyncMutex::new("marshal.context", context)),
207 application,
208 marshal,
209 epocher,
210 gates: Gates::new(),
211 build_duration,
212 proposal_parent_fetch_duration,
213 ancestor_fetch_duration,
214 }
215 }
216}
217
218impl<E, S, A, B, ES> Automaton for Inline<E, S, A, B, ES>
219where
220 E: Rng + Spawner + Metrics + Clock,
221 S: Scheme,
222 A: Application<E, Block = B, SigningScheme = S, Context = Context<B::Digest, S::PublicKey>>,
223 B: Block + Clone,
224 ES: Epocher,
225{
226 type Digest = B::Digest;
227 type Context = Context<Self::Digest, S::PublicKey>;
228
229 #[allow(clippy::async_yields_async)]
238 #[tracing::instrument(name = "marshal.inline.propose", level = "info", skip_all, fields(round = %consensus_context.round))]
239 async fn propose(
240 &mut self,
241 consensus_context: Context<Self::Digest, S::PublicKey>,
242 ) -> oneshot::Receiver<Self::Digest> {
243 let marshal = self.marshal.clone();
244 let mut application = self.application.clone();
245 let epocher = self.epocher.clone();
246 let gates = self.gates.clone();
247 let build_duration = self.build_duration.clone();
248 let proposal_parent_fetch_duration = self.proposal_parent_fetch_duration.clone();
249 let ancestor_fetch_duration = self.ancestor_fetch_duration.clone();
250
251 let (mut tx, rx) = oneshot::channel();
252 let context = self
253 .context
254 .lock()
255 .await
256 .child("propose")
257 .with_attribute("round", consensus_context.round);
258 let span = info_span!(
259 "marshal.inline.propose.task",
260 round = %consensus_context.round
261 );
262 context.spawn(move |runtime_context| {
263 async move {
264 if marshal
276 .get_verified(consensus_context.round)
277 .await
278 .is_some()
279 {
280 debug!(
281 round = ?consensus_context.round,
282 "skipping proposal: verified block already exists for round on restart"
283 );
284 return;
285 }
286
287 let (parent_view, parent_commitment) = consensus_context.parent;
296 let parent_request = marshal.subscribe_by_commitment(
297 parent_commitment,
298 CommitmentFallback::FetchByRound {
299 round: Round::new(consensus_context.epoch(), parent_view),
300 },
301 );
302
303 let parent_timer = proposal_parent_fetch_duration.timer(&runtime_context);
304 let parent = select! {
305 _ = tx.closed() => {
306 debug!(reason = "consensus dropped receiver", "skipping proposal");
307 return;
308 },
309 result = parent_request => match result {
310 Ok(parent) => parent,
311 Err(_) => {
312 debug!(
313 ?parent_commitment,
314 reason = "failed to fetch parent block",
315 "skipping proposal"
316 );
317 return;
318 }
319 },
320 };
321 parent_timer.observe(&runtime_context);
322
323 let last_in_epoch = epocher
325 .last(consensus_context.epoch())
326 .expect("current epoch should exist");
327 if parent.height() == last_in_epoch {
328 let digest = parent.digest();
329 gates
330 .stage(
331 consensus_context.round,
332 digest,
333 parent,
334 tx,
335 "re-proposed boundary block",
336 )
337 .await;
338 return;
339 }
340
341 let ancestor_stream = marshal.ancestor_stream(
342 Arc::new(runtime_context.child("ancestor_stream")),
343 [parent],
344 ancestor_fetch_duration,
345 );
346 let build_request = application
347 .propose(
348 (
349 runtime_context.child("app_propose"),
350 consensus_context.clone(),
351 ),
352 ancestor_stream,
353 )
354 .instrument(info_span!(
355 "marshal.inline.application.propose",
356 round = %consensus_context.round,
357 parent_view = parent_view.traced(),
358 parent = %parent_commitment
359 ));
360
361 let build_timer = build_duration.timer(&runtime_context);
362 let built_block = select! {
363 _ = tx.closed() => {
364 debug!(reason = "consensus dropped receiver", "skipping proposal");
365 return;
366 },
367 result = build_request => match result {
368 Some(block) => block,
369 None => {
370 debug!(
371 ?parent_commitment,
372 reason = "block building failed",
373 "skipping proposal"
374 );
375 return;
376 }
377 },
378 };
379 build_timer.observe(&runtime_context);
380
381 let digest = built_block.digest();
382 gates
383 .stage(
384 consensus_context.round,
385 digest,
386 Arc::new(built_block),
387 tx,
388 "proposed block",
389 )
390 .await;
391 }
392 .instrument(span)
393 });
394 rx
395 }
396
397 #[allow(clippy::async_yields_async)]
410 #[tracing::instrument(name = "marshal.inline.verify", level = "info", skip_all, fields(round = %context.round, digest = %digest))]
411 async fn verify(
412 &mut self,
413 context: Context<Self::Digest, S::PublicKey>,
414 digest: Self::Digest,
415 ) -> oneshot::Receiver<bool> {
416 let round = context.round;
421 let (durable_tx, durable_rx) = oneshot::channel();
422 self.gates.insert(round, digest, durable_rx);
423
424 let marshal = self.marshal.clone();
425 let mut application = self.application.clone();
426 let epocher = self.epocher.clone();
427 let ancestor_fetch_duration = self.ancestor_fetch_duration.clone();
428
429 let (mut tx, rx) = oneshot::channel();
430 let runtime_context = self
431 .context
432 .lock()
433 .await
434 .child("inline_verify")
435 .with_attribute("round", round);
436 let span = info_span!(
437 "marshal.inline.verify.task",
438 round = %round,
439 digest = %digest
440 );
441 runtime_context.spawn(move |runtime_context| {
442 async move {
443 let parent_request = (digest != context.parent.1).then(|| {
450 let (parent_view, parent_commitment) = context.parent;
451 marshal.subscribe_by_commitment(
452 parent_commitment,
453 CommitmentFallback::FetchByRound {
454 round: Round::new(context.epoch(), parent_view),
455 },
456 )
457 });
458
459 let block_request = marshal.subscribe_by_digest(digest, DigestFallback::Wait);
460 let Some(block) =
461 await_block_subscription(&mut tx, block_request, &digest, "verification").await
462 else {
463 return;
464 };
465
466 let Some(decision) =
474 precheck_epoch_and_reproposal(&epocher, &marshal, &context, digest, block)
475 .await
476 else {
477 return;
478 };
479 let block = match decision {
480 Decision::Complete(valid) => {
481 tx.send_lossy(valid);
484 durable_tx.send_lossy(valid);
485 return;
486 }
487 Decision::Continue(block) => block,
488 };
489
490 let parent_request =
493 parent_request.expect("non-reproposal has a parent subscription");
494
495 let store = marshal.verified(round, Arc::clone(&block));
511 let verify_then_vote = async {
512 let parent = match await_and_validate_parent(
515 context.parent.1,
516 block.as_ref(),
517 parent_request,
518 &mut tx,
519 )
520 .await
521 {
522 Some(ParentCheck::Valid(parent)) => parent,
523 Some(ParentCheck::Invalid) => {
524 tx.send_lossy(false);
525 return Some(false);
526 }
527 None => return None,
528 };
529 let valid = run_app_verify(
530 runtime_context,
531 context,
532 Arc::clone(&block),
533 parent,
534 &mut application,
535 &marshal,
536 &mut tx,
537 ancestor_fetch_duration,
538 )
539 .await;
540 if let Some(valid) = valid {
541 tx.send_lossy(valid);
542 }
543 valid
544 };
545 let (verdict, durable) = futures::join!(verify_then_vote, store);
546 if let Some(valid) = gates::resolve(verdict, durable) {
547 durable_tx.send_lossy(valid);
548 }
549 }
550 .instrument(span)
551 });
552 rx
553 }
554}
555
556impl<E, S, A, B, ES> CertifiableAutomaton for Inline<E, S, A, B, ES>
559where
560 E: Rng + Spawner + Metrics + Clock,
561 S: Scheme,
562 A: Application<E, Block = B, SigningScheme = S, Context = Context<B::Digest, S::PublicKey>>,
563 B: Block + Clone,
564 ES: Epocher,
565{
566 #[allow(clippy::async_yields_async)]
567 #[tracing::instrument(name = "marshal.inline.certify", level = "info", skip_all, fields(round = %round, digest = %digest))]
568 async fn certify(&mut self, round: Round, digest: Self::Digest) -> oneshot::Receiver<bool> {
569 self.gates.flush_unrelayed(&self.marshal, round, digest);
570
571 let task = self.gates.take(round, digest);
576
577 if task.is_some() {
582 self.marshal.hint_notarized(round, digest);
583 }
584 let marshal = self.marshal.clone();
585 let (mut tx, rx) = oneshot::channel();
586 let context = self
587 .context
588 .lock()
589 .await
590 .child("inline_certify")
591 .with_attribute("round", round);
592 context.spawn(move |_| {
593 async move {
594 if let Some(task) = task {
597 let result = select! {
598 _ = tx.closed() => {
599 debug!(reason = "consensus dropped receiver", "skipping certification");
600 return;
601 },
602 result = task => result,
603 };
604 if let Ok(verdict) = result {
605 tx.send_lossy(verdict);
606 return;
607 }
608 }
609
610 let block_rx =
616 marshal.subscribe_by_digest(digest, DigestFallback::FetchByRound { round });
617 let Some(block) =
618 await_block_subscription(&mut tx, block_rx, &digest, "certification").await
619 else {
620 return;
621 };
622 if !marshal.certified(round, block).await {
623 return;
624 }
625 tx.send_lossy(true);
626 }
627 .instrument(info_span!(
628 "marshal.inline.certify.task",
629 round = %round,
630 digest = %digest
631 ))
632 });
633
634 rx
635 }
636}
637
638impl<E, S, A, B, ES> Relay for Inline<E, S, A, B, ES>
639where
640 E: Rng + Spawner + Metrics + Clock,
641 S: Scheme,
642 A: Application<E, Block = B, Context = Context<B::Digest, S::PublicKey>>,
643 B: Block + Clone,
644 ES: Epocher,
645{
646 type Digest = B::Digest;
647 type PublicKey = S::PublicKey;
648 type Plan = Plan<S::PublicKey>;
649
650 fn broadcast(&mut self, commitment: Self::Digest, plan: Plan<S::PublicKey>) -> Feedback {
651 relay::broadcast(&self.gates, &self.marshal, commitment, plan)
652 }
653}
654
655impl<E, S, A, B, ES> Reporter for Inline<E, S, A, B, ES>
656where
657 E: Rng + Spawner + Metrics + Clock,
658 S: Scheme,
659 A: Application<E, Block = B, Context = Context<B::Digest, S::PublicKey>>
660 + Reporter<Activity = Update<B>>,
661 B: Block + Clone,
662 ES: Epocher,
663{
664 type Activity = A::Activity;
665
666 fn report(&mut self, update: Self::Activity) -> Feedback {
668 if let Update::Tip(tip_round, _, _) = &update {
669 self.gates.retain_after(tip_round);
670 }
671 self.application.report(update)
672 }
673}
674
675#[cfg(test)]
676mod tests {
677 use super::Inline;
678 use crate::{
679 marshal::mocks::{
680 harness::{
681 default_leader, make_raw_block, setup_network_with_participants, Ctx,
682 StandardHarness, TestHarness, B, BLOCKS_PER_EPOCH, NAMESPACE, NUM_VALIDATORS, S, V,
683 },
684 verifying::{GatedVerifyingApp, MockVerifyingApp},
685 },
686 simplex::{scheme::bls12381_threshold::vrf as bls12381_threshold_vrf, types::Context},
687 types::{Epoch, FixedEpocher, Height, Round, View},
688 Application, Automaton, Block, CertifiableAutomaton, Relay,
689 };
690 use commonware_broadcast::Broadcaster;
691 use commonware_cryptography::{
692 certificate::{mocks::Fixture, ConstantProvider, Scheme},
693 sha256::Sha256,
694 Digestible, Hasher as _,
695 };
696 use commonware_macros::{select, test_traced};
697 use commonware_runtime::{deterministic, Clock, Metrics, Runner, Spawner, Supervisor as _};
698 use commonware_utils::{channel::fallible::OneshotExt, NZUsize};
699 use rand::Rng;
700 use std::time::Duration;
701
702 #[allow(dead_code)]
704 fn assert_non_certifiable_block_supported<E, S, A, B, ES>()
705 where
706 E: Rng + Spawner + Metrics + Clock,
707 S: Scheme,
708 A: Application<E, Block = B, SigningScheme = S, Context = Context<B::Digest, S::PublicKey>>,
709 B: Block + Clone,
710 ES: crate::types::Epocher,
711 {
712 fn assert_automaton<T: Automaton>() {}
713 fn assert_certifiable<T: CertifiableAutomaton>() {}
714 fn assert_relay<T: Relay>() {}
715
716 assert_automaton::<Inline<E, S, A, B, ES>>();
717 assert_certifiable::<Inline<E, S, A, B, ES>>();
718 assert_relay::<Inline<E, S, A, B, ES>>();
719 }
720
721 #[test_traced("INFO")]
722 fn test_certify_returns_immediately_after_verify_fetches_block() {
723 let runner = deterministic::Runner::timed(Duration::from_secs(30));
724 runner.start(|mut context| async move {
725 let Fixture {
726 participants,
727 schemes,
728 ..
729 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
730 let mut oracle = setup_network_with_participants(
731 context.child("network"),
732 NZUsize!(1),
733 participants.clone(),
734 )
735 .await;
736
737 let me = participants[0].clone();
738 let setup = StandardHarness::setup_validator(
739 context.child("validator").with_attribute("index", 0),
740 &mut oracle,
741 me.clone(),
742 ConstantProvider::new(schemes[0].clone()),
743 )
744 .await;
745 let marshal = setup.mailbox;
746
747 let genesis = make_raw_block(Sha256::hash(b""), Height::zero(), 0);
748 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
749 let mut inline = Inline::new(
750 context.child("inline"),
751 mock_app,
752 marshal.clone(),
753 FixedEpocher::new(BLOCKS_PER_EPOCH),
754 );
755
756 let parent_round = Round::new(Epoch::zero(), View::new(1));
758 let parent_ctx = Ctx {
759 round: parent_round,
760 leader: default_leader(),
761 parent: (View::zero(), genesis.digest()),
762 };
763 let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
764 let parent_digest = parent.digest();
765 assert!(marshal.verified(parent_round, parent).await);
766
767 let round = Round::new(Epoch::zero(), View::new(2));
768 let verify_context = Ctx {
769 round,
770 leader: me,
771 parent: (View::new(1), parent_digest),
772 };
773 let block =
774 B::new::<Sha256>(verify_context.clone(), parent_digest, Height::new(2), 200);
775 let digest = block.digest();
776 assert!(marshal.verified(round, block).await);
777
778 let verify_rx = inline.verify(verify_context, digest).await;
780 assert!(
781 verify_rx.await.unwrap(),
782 "verify should complete successfully before certify"
783 );
784
785 let certify_rx = inline.certify(round, digest).await;
787
788 select! {
789 result = certify_rx => {
790 assert!(
791 result.unwrap(),
792 "certify should return immediately once verify has fetched the block"
793 );
794 },
795 _ = context.sleep(Duration::from_secs(5)) => {
796 panic!("certify should not hang after local verify completed");
797 },
798 }
799 });
800 }
801
802 #[test_traced("INFO")]
803 fn test_certify_succeeds_without_verify_task() {
804 let runner = deterministic::Runner::timed(Duration::from_secs(30));
805 runner.start(|mut context| async move {
806 let Fixture {
807 participants,
808 schemes,
809 ..
810 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
811 let mut oracle = setup_network_with_participants(
812 context.child("network"),
813 NZUsize!(1),
814 participants.clone(),
815 )
816 .await;
817
818 let me = participants[0].clone();
819 let setup = StandardHarness::setup_validator(
820 context.child("validator").with_attribute("index", 0),
821 &mut oracle,
822 me.clone(),
823 ConstantProvider::new(schemes[0].clone()),
824 )
825 .await;
826 let marshal = setup.mailbox;
827
828 let genesis = make_raw_block(Sha256::hash(b""), Height::zero(), 0);
829 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
830 let mut inline = Inline::new(
831 context.child("inline"),
832 mock_app,
833 marshal.clone(),
834 FixedEpocher::new(BLOCKS_PER_EPOCH),
835 );
836
837 let parent_round = Round::new(Epoch::zero(), View::new(1));
839 let parent_ctx = Ctx {
840 round: parent_round,
841 leader: default_leader(),
842 parent: (View::zero(), genesis.digest()),
843 };
844 let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
845 let parent_digest = parent.digest();
846 assert!(marshal.verified(parent_round, parent).await);
847
848 let round = Round::new(Epoch::zero(), View::new(2));
849 let verify_context = Ctx {
850 round,
851 leader: me,
852 parent: (View::new(1), parent_digest),
853 };
854 let block =
855 B::new::<Sha256>(verify_context.clone(), parent_digest, Height::new(2), 200);
856 let digest = block.digest();
857 assert!(marshal.verified(round, block).await);
858
859 let certify_rx = inline.certify(round, digest).await;
861
862 select! {
863 result = certify_rx => {
864 assert!(
865 result.unwrap(),
866 "certify should resolve once block availability is known"
867 );
868 },
869 _ = context.sleep(Duration::from_secs(5)) => {
870 panic!("certify should not hang when block is already available in marshal");
871 },
872 }
873 });
874 }
875
876 #[test_traced("INFO")]
877 fn test_certify_reproposal_uses_available_blocks_after_verify() {
878 let runner = deterministic::Runner::timed(Duration::from_secs(30));
879 runner.start(|mut context| async move {
880 let Fixture {
881 participants,
882 schemes,
883 ..
884 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
885 let mut oracle =
886 setup_network_with_participants(context.child("network"), NZUsize!(1), participants.clone())
887 .await;
888
889 let me = participants[0].clone();
890 let setup = StandardHarness::setup_validator(
891 context.child("validator").with_attribute("index", 0),
892 &mut oracle,
893 me.clone(),
894 ConstantProvider::new(schemes[0].clone()),
895 )
896 .await;
897 let marshal = setup.mailbox;
898 let marshal_actor_handle = setup.actor_handle;
899
900 let genesis = make_raw_block(Sha256::hash(b""), Height::zero(), 0);
901 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
902 let mut inline = Inline::new(context.child("inline"),
903 mock_app,
904 marshal.clone(),
905 FixedEpocher::new(BLOCKS_PER_EPOCH),
906 );
907
908 let boundary_height = Height::new(BLOCKS_PER_EPOCH.get() - 1);
909 let boundary_round = Round::new(Epoch::zero(), View::new(boundary_height.get()));
910 let boundary_block = B::new::<Sha256>(
911 Ctx {
912 round: boundary_round,
913 leader: default_leader(),
914 parent: (View::zero(), genesis.digest()),
915 },
916 genesis.digest(),
917 boundary_height,
918 1900,
919 );
920 let boundary_digest = boundary_block.digest();
921 assert!(
922 marshal.verified(boundary_round, boundary_block).await
923 );
924
925 let reproposal_round = Round::new(Epoch::zero(), View::new(boundary_height.get() + 1));
926 let reproposal_context = Ctx {
927 round: reproposal_round,
928 leader: me,
929 parent: (View::new(boundary_height.get()), boundary_digest),
930 };
931
932 let verify_rx = inline.verify(reproposal_context, boundary_digest).await;
933 assert!(
934 verify_rx.await.unwrap(),
935 "verify should accept a valid boundary re-proposal"
936 );
937
938 marshal_actor_handle.abort();
939 drop(marshal);
940 context.sleep(Duration::from_millis(1)).await;
941
942 let certify_rx = inline.certify(reproposal_round, boundary_digest).await;
943 select! {
944 result = certify_rx => {
945 assert!(
946 result.unwrap(),
947 "certify should use the available_blocks fast path for verified re-proposals"
948 );
949 },
950 _ = context.sleep(Duration::from_secs(5)) => {
951 panic!("certify should not depend on marshal after verify cached a re-proposal");
952 },
953 }
954 });
955 }
956
957 #[test_traced("WARN")]
961 fn test_inline_certify_persists_block_before_resolving() {
962 for seed in 0u64..16 {
963 inline_certify_persists_block_before_resolving_at(seed);
964 }
965 }
966
967 fn inline_certify_persists_block_before_resolving_at(seed: u64) {
968 let runner = deterministic::Runner::new(
969 deterministic::Config::new()
970 .with_seed(seed)
971 .with_timeout(Some(Duration::from_secs(60))),
972 );
973 runner.start(|mut context| async move {
974 let Fixture {
975 participants,
976 schemes,
977 ..
978 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
979 let mut oracle = setup_network_with_participants(
980 context.child("network"),
981 NZUsize!(1),
982 participants.clone(),
983 )
984 .await;
985
986 let me = participants[0].clone();
987
988 let setup = StandardHarness::setup_validator(
989 context.child("validator").with_attribute("index", 0),
990 &mut oracle,
991 me.clone(),
992 ConstantProvider::new(schemes[0].clone()),
993 )
994 .await;
995 let marshal = setup.mailbox;
996 let buffer = setup.extra;
997 let actor_handle = setup.actor_handle;
998
999 let genesis = make_raw_block(Sha256::hash(b""), Height::zero(), 0);
1000 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1001 let mut inline = Inline::new(
1002 context.child("inline"),
1003 mock_app,
1004 marshal.clone(),
1005 FixedEpocher::new(BLOCKS_PER_EPOCH),
1006 );
1007
1008 let parent = make_raw_block(genesis.digest(), Height::new(1), 100);
1009 let parent_digest = parent.digest();
1010
1011 let child_round = Round::new(Epoch::zero(), View::new(2));
1012 let child_ctx = Ctx {
1013 round: child_round,
1014 leader: me.clone(),
1015 parent: (View::new(1), parent_digest),
1016 };
1017 let child = B::new::<Sha256>(child_ctx.clone(), parent_digest, Height::new(2), 200);
1018 let child_digest = child.digest();
1019
1020 assert!(
1021 buffer
1022 .broadcast(commonware_p2p::Recipients::Some(vec![]), parent.clone())
1023 .accepted(),
1024 "buffer broadcast for parent should be accepted"
1025 );
1026 assert!(
1027 buffer
1028 .broadcast(commonware_p2p::Recipients::Some(vec![]), child.clone())
1029 .accepted(),
1030 "buffer broadcast for child should be accepted"
1031 );
1032
1033 let verify_rx = inline.verify(child_ctx, child_digest).await;
1034 let certify_result = inline
1035 .certify(child_round, child_digest)
1036 .await
1037 .await
1038 .expect("certify result missing");
1039 assert!(certify_result, "certify should succeed");
1040
1041 actor_handle.abort();
1042 drop(verify_rx);
1043 drop(inline);
1044 drop(marshal);
1045 drop(buffer);
1046
1047 let setup2 = StandardHarness::setup_validator(
1048 context
1049 .child("validator_restart")
1050 .with_attribute("index", 0),
1051 &mut oracle,
1052 me.clone(),
1053 ConstantProvider::new(schemes[0].clone()),
1054 )
1055 .await;
1056 let marshal2 = setup2.mailbox;
1057
1058 let post_restart = marshal2.get_block(&child_digest).await;
1059 assert!(
1060 post_restart.is_some(),
1061 "certify resolved true so block must be durably persisted (seed={seed})"
1062 );
1063 });
1064 }
1065
1066 #[test_traced("WARN")]
1072 fn test_inline_propose_then_certify_persists_block() {
1073 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1074 runner.start(|mut context| async move {
1075 let Fixture {
1076 participants,
1077 schemes,
1078 ..
1079 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1080 let mut oracle = setup_network_with_participants(
1081 context.child("network"),
1082 NZUsize!(1),
1083 participants.clone(),
1084 )
1085 .await;
1086
1087 let me = participants[0].clone();
1088 let setup = StandardHarness::setup_validator(
1089 context.child("validator").with_attribute("index", 0),
1090 &mut oracle,
1091 me.clone(),
1092 ConstantProvider::new(schemes[0].clone()),
1093 )
1094 .await;
1095 let marshal = setup.mailbox;
1096 let actor_handle = setup.actor_handle;
1097
1098 let genesis = make_raw_block(Sha256::hash(b""), Height::zero(), 0);
1099
1100 let parent_round = Round::new(Epoch::zero(), View::new(1));
1102 let parent_ctx = Ctx {
1103 round: parent_round,
1104 leader: default_leader(),
1105 parent: (View::zero(), genesis.digest()),
1106 };
1107 let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
1108 let parent_digest = parent.digest();
1109 assert!(marshal.verified(parent_round, parent).await);
1110
1111 let round = Round::new(Epoch::zero(), View::new(2));
1113 let ctx = Ctx {
1114 round,
1115 leader: me.clone(),
1116 parent: (View::new(1), parent_digest),
1117 };
1118 let child = B::new::<Sha256>(ctx.clone(), parent_digest, Height::new(2), 200);
1119 let child_digest = child.digest();
1120 let mock_app: MockVerifyingApp<B, S> =
1121 MockVerifyingApp::new().with_propose_result(child);
1122 let mut inline = Inline::new(
1123 context.child("inline"),
1124 mock_app,
1125 marshal.clone(),
1126 FixedEpocher::new(BLOCKS_PER_EPOCH),
1127 );
1128
1129 let digest = inline
1130 .propose(ctx)
1131 .await
1132 .await
1133 .expect("propose must return a digest");
1134 assert_eq!(
1135 digest, child_digest,
1136 "propose must return the built block's digest"
1137 );
1138
1139 assert!(
1141 inline
1142 .certify(round, child_digest)
1143 .await
1144 .await
1145 .expect("certify result missing"),
1146 "certify must succeed for the leader's own proposal"
1147 );
1148
1149 actor_handle.abort();
1151 drop(inline);
1152 drop(marshal);
1153
1154 let setup2 = StandardHarness::setup_validator(
1155 context
1156 .child("validator_restart")
1157 .with_attribute("index", 0),
1158 &mut oracle,
1159 me,
1160 ConstantProvider::new(schemes[0].clone()),
1161 )
1162 .await;
1163 let marshal2 = setup2.mailbox;
1164
1165 assert!(
1166 marshal2.get_block(&child_digest).await.is_some(),
1167 "certify resolved true for the leader's own proposal so the block must be durable"
1168 );
1169 });
1170 }
1171
1172 #[test_traced("WARN")]
1176 fn test_inline_certify_recovers_after_verify_receiver_drop() {
1177 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1178 runner.start(|mut context| async move {
1179 let Fixture {
1180 participants,
1181 schemes,
1182 ..
1183 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1184 let mut oracle = setup_network_with_participants(
1185 context.child("network"),
1186 NZUsize!(1),
1187 participants.clone(),
1188 )
1189 .await;
1190
1191 let me = participants[0].clone();
1192 let setup = StandardHarness::setup_validator(
1193 context.child("validator").with_attribute("index", 0),
1194 &mut oracle,
1195 me.clone(),
1196 ConstantProvider::new(schemes[0].clone()),
1197 )
1198 .await;
1199 let marshal = setup.mailbox;
1200
1201 let genesis = make_raw_block(Sha256::hash(b""), Height::zero(), 0);
1202 let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1203 let mut inline = Inline::new(
1204 context.child("inline"),
1205 mock_app,
1206 marshal.clone(),
1207 FixedEpocher::new(BLOCKS_PER_EPOCH),
1208 );
1209
1210 let round = Round::new(Epoch::zero(), View::new(1));
1211 let block_context = Ctx {
1212 round,
1213 leader: me,
1214 parent: (View::zero(), genesis.digest()),
1215 };
1216 let block =
1217 B::new::<Sha256>(block_context.clone(), genesis.digest(), Height::new(1), 100);
1218 let digest = block.digest();
1219
1220 let verify_rx = inline.verify(block_context, digest).await;
1221 drop(verify_rx);
1222
1223 context.sleep(Duration::from_millis(10)).await;
1226
1227 assert!(marshal.verified(round, block).await);
1228 let certify_rx = inline.certify(round, digest).await;
1229 select! {
1230 result = certify_rx => {
1231 assert!(
1232 result.expect("certify result missing"),
1233 "certify should recover after verify receiver drop"
1234 );
1235 },
1236 _ = context.sleep(Duration::from_secs(5)) => {
1237 panic!("certify should recover promptly after verify drop");
1238 },
1239 }
1240 });
1241 }
1242
1243 #[test_traced("WARN")]
1250 fn test_inline_store_overlaps_app_verify() {
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 )
1263 .await;
1264
1265 let me = participants[0].clone();
1266
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 let buffer = setup.extra;
1276
1277 let genesis = make_raw_block(Sha256::hash(b""), Height::zero(), 0);
1278 let (mock_app, verify_started, release_verify): (GatedVerifyingApp<B, S>, _, _) =
1279 GatedVerifyingApp::new();
1280 let mut inline = Inline::new(
1281 context.child("inline"),
1282 mock_app,
1283 marshal.clone(),
1284 FixedEpocher::new(BLOCKS_PER_EPOCH),
1285 );
1286
1287 let parent = make_raw_block(genesis.digest(), Height::new(1), 100);
1288 let parent_digest = parent.digest();
1289
1290 let child_round = Round::new(Epoch::zero(), View::new(2));
1291 let child_ctx = Ctx {
1292 round: child_round,
1293 leader: me.clone(),
1294 parent: (View::new(1), parent_digest),
1295 };
1296 let child = B::new::<Sha256>(child_ctx.clone(), parent_digest, Height::new(2), 200);
1297 let child_digest = child.digest();
1298
1299 assert!(
1300 buffer
1301 .broadcast(commonware_p2p::Recipients::Some(vec![]), parent)
1302 .accepted(),
1303 "buffer broadcast for parent should be accepted"
1304 );
1305 assert!(
1306 buffer
1307 .broadcast(commonware_p2p::Recipients::Some(vec![]), child)
1308 .accepted(),
1309 "buffer broadcast for child should be accepted"
1310 );
1311
1312 let verify_rx = inline.verify(child_ctx, child_digest).await;
1313
1314 verify_started
1318 .await
1319 .expect("verify should reach the gated application");
1320 assert!(
1321 marshal.get_block(&child_digest).await.is_some(),
1322 "the store request runs concurrently with app.verify, so the block is locally queryable while verification is still gated"
1323 );
1324
1325 release_verify.send_lossy(());
1328 assert!(
1329 verify_rx.await.expect("verify result missing"),
1330 "inline verify should pass once verification is released"
1331 );
1332 let certify_rx = inline.certify(child_round, child_digest).await;
1333 select! {
1334 result = certify_rx => {
1335 assert!(
1336 result.expect("certify result missing"),
1337 "certify should succeed once verification passes"
1338 );
1339 },
1340 _ = context.sleep(Duration::from_secs(5)) => {
1341 panic!("certify should resolve after verification is released");
1342 },
1343 }
1344 });
1345 }
1346
1347 #[test_traced("WARN")]
1358 fn test_propose_skips_when_verified_block_exists_on_restart() {
1359 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1360 runner.start(|mut context| async move {
1361 let Fixture {
1362 participants,
1363 schemes,
1364 ..
1365 } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1366 let mut oracle = setup_network_with_participants(
1367 context.child("network"),
1368 NZUsize!(1),
1369 participants.clone(),
1370 )
1371 .await;
1372
1373 let me = participants[0].clone();
1374 let round = Round::new(Epoch::zero(), View::new(1));
1375 let genesis = make_raw_block(Sha256::hash(b""), Height::zero(), 0);
1376 let ctx = Ctx {
1377 round,
1378 leader: me.clone(),
1379 parent: (View::zero(), genesis.digest()),
1380 };
1381
1382 let pre_setup = StandardHarness::setup_validator(
1386 context.child("validator").with_attribute("index", 0),
1387 &mut oracle,
1388 me.clone(),
1389 ConstantProvider::new(schemes[0].clone()),
1390 )
1391 .await;
1392 let pre_marshal = pre_setup.mailbox;
1393 let pre_actor = pre_setup.actor_handle;
1394 let pre_extra = pre_setup.extra;
1395 let pre_application = pre_setup.application;
1396
1397 let stale_block = B::new::<Sha256>(ctx.clone(), genesis.digest(), Height::new(1), 100);
1398 assert!(pre_marshal.verified(round, stale_block).await);
1399
1400 pre_actor.abort();
1403 drop(pre_marshal);
1404 drop(pre_extra);
1405 drop(pre_application);
1406
1407 let post_setup = StandardHarness::setup_validator(
1411 context
1412 .child("validator_restart")
1413 .with_attribute("index", 0),
1414 &mut oracle,
1415 me.clone(),
1416 ConstantProvider::new(schemes[0].clone()),
1417 )
1418 .await;
1419 let post_marshal = post_setup.mailbox;
1420
1421 let fresh_block = B::new::<Sha256>(ctx.clone(), genesis.digest(), Height::new(1), 200);
1422 let mock_app: MockVerifyingApp<B, S> =
1423 MockVerifyingApp::new().with_propose_result(fresh_block);
1424 let mut inline = Inline::new(
1425 context.child("inline"),
1426 mock_app,
1427 post_marshal.clone(),
1428 FixedEpocher::new(BLOCKS_PER_EPOCH),
1429 );
1430
1431 let digest_rx = inline.propose(ctx).await;
1432 assert!(
1433 digest_rx.await.is_err(),
1434 "propose must drop the receiver so the voter nullifies the round via timeout"
1435 );
1436 });
1437 }
1438}