Skip to main content

commonware_consensus/marshal/standard/
deferred.rs

1//! Wrapper for consensus applications that handles epochs and block dissemination.
2//!
3//! # Overview
4//!
5//! [`Deferred`] is an adapter that wraps any [`Application`] implementation to handle
6//! epoch transitions automatically. It intercepts consensus operations (propose, verify) and
7//! ensures blocks are only produced within valid epoch boundaries.
8//!
9//! # Epoch Boundaries
10//!
11//! When the parent is the last block in an epoch (as determined by the [`Epocher`]), this wrapper
12//! re-proposes that boundary block instead of building a new block. This avoids producing blocks
13//! that would be pruned by the epoch transition.
14//!
15//! # Deferred Verification
16//!
17//! Before casting a notarize vote, [`Deferred`] waits for the block to become available and
18//! then verifies that the block's embedded context matches the consensus context. However, it does not
19//! wait for the application to finish verifying the block contents before voting. This enables verification
20//! to run while we wait for a quorum of votes to form a certificate (hiding verification latency behind network
21//! latency). Once a certificate is formed, we wait on the verification result in [`CertifiableAutomaton::certify`]
22//! before voting to finalize (ensuring no invalid blocks are admitted to the canonical chain).
23//!
24//! # Usage
25//!
26//! Wrap your [`Application`] implementation with [`Deferred::new`] and provide it to your
27//! consensus engine for the [`Automaton`] and [`Relay`]. The wrapper handles all epoch logic transparently.
28//!
29//! ```rust,ignore
30//! let application = Deferred::new(
31//!     context,
32//!     my_application,
33//!     marshal_mailbox,
34//!     epocher,
35//! );
36//! ```
37//!
38//! # Implementation Notes
39//!
40//! - Genesis blocks are handled specially: epoch 0 returns the application's genesis block,
41//!   while subsequent epochs use the last block of the previous epoch as genesis
42//! - Blocks are automatically verified to be within the current epoch
43//!
44//! # Notarization and Data Availability
45//!
46//! In rare crash cases, it is possible for a notarization certificate to exist without a block being
47//! available to the honest parties. [`CertifiableAutomaton::certify`] may then remain pending while
48//! it waits for the block. Simplex may time out and nullify the view without resolving that request.
49//!
50//! For this reason, it should not be expected that every notarized payload will be certifiable due
51//! to the lack of an available block. However, if even one honest and online party has the block,
52//! they will attempt to forward it to others via marshal's resolver.
53//!
54//! ```text
55//!                                      ┌───────────────────────────────────────────────────┐
56//!                                      ▼                                                   │
57//! ┌─────────────────────┐   ┌─────────────────────┐   ┌─────────────────────┐   ┌─────────────────────┐
58//! │          B1         │◀──│          B2         │◀──│          B3         │XXX│          B4         │
59//! └─────────────────────┘   └─────────────────────┘   └──────────┬──────────┘   └─────────────────────┘
60//!                                                                │
61//!                                                         Pending Certify
62//! ```
63//!
64//! # Future Work
65//!
66//! - To further reduce view latency, a participant could optimistically vote for a block prior to
67//!   observing its availability during [`Automaton::verify`]. However, this would require updating
68//!   other components (like [`crate::marshal`]) to handle backfill where notarization does not imply
69//!   a block is fetchable (without modification, a malicious leader that withholds blocks during propose
70//!   could get an honest node to exhaust their network rate limit fetching things that don't exist rather
71//!   than blocks they need AND can fetch).
72
73use crate::{
74    Application, Automaton, CertifiableAutomaton, CertifiableBlock, Epochable, Relay, Reporter,
75    marshal::{
76        Update,
77        application::{
78            gates::{self, GateOutcome, Gates},
79            validation::{Stage, is_inferred_reproposal_at_certify},
80        },
81        core::{CommitmentFallback, DigestFallback, Mailbox},
82        standard::{
83            Standard, relay,
84            validation::{
85                Decision, ParentCheck, await_and_validate_parent, precheck_epoch_and_reproposal,
86                run_app_verify,
87            },
88        },
89    },
90    simplex::{Plan, types::Context},
91    types::{Epocher, Round},
92};
93use commonware_actor::Feedback;
94use commonware_cryptography::{Digestible, certificate::Scheme};
95use commonware_macros::select;
96use commonware_runtime::{
97    Clock, Metrics, Spawner,
98    telemetry::{
99        metrics::{
100            MetricsExt as _,
101            histogram::{Buckets, Timed},
102        },
103        traces::TracedExt as _,
104    },
105};
106use commonware_utils::{
107    channel::{fallible::OneshotExt, oneshot},
108    sync::TracedAsyncMutex,
109};
110use rand_core::Rng;
111use std::sync::Arc;
112use tracing::{Instrument as _, debug, info_span};
113
114/// An [`Application`] adapter that handles epoch transitions and validates block ancestry.
115///
116/// This wrapper intercepts consensus operations to enforce epoch boundaries and validate
117/// block ancestry. It prevents blocks from being produced outside their valid epoch,
118/// handles the special case of re-proposing boundary blocks at epoch boundaries,
119/// and ensures all blocks have valid parent linkage and contiguous heights.
120///
121/// # Ancestry Validation
122///
123/// Applications wrapped by [`Deferred`] can rely on the following ancestry checks being
124/// performed automatically during verification:
125/// - Parent digest matches the consensus context's expected parent
126/// - Block height is exactly one greater than the parent's height
127///
128/// Verifying only the immediate parent is sufficient since the parent was either notarized by
129/// consensus, whose honest voters verified its context, or verified locally before this
130/// participant voted for it (optimistic validation). Either way the entire ancestry chain back
131/// to genesis is transitively validated.
132///
133/// Applications do not need to re-implement these checks in their own verification logic.
134///
135/// # Context Recovery
136///
137/// With deferred verification, validators wait for data availability (DA) and verify the context
138/// before voting. If a validator crashes after voting but before certification, they lose their in-memory
139/// certification gate task. When recovering, validators extract context from a [`CertifiableBlock`].
140///
141/// _This embedded context is trustworthy because the notarizing quorum (which contains at least f+1 honest
142/// validators) verified that the block's context matched the consensus context before voting._
143pub struct Deferred<E, S, A, B, ES>
144where
145    E: Rng + Spawner + Metrics + Clock,
146    S: Scheme,
147    A: Application<E>,
148    B: CertifiableBlock,
149    ES: Epocher,
150{
151    context: Arc<TracedAsyncMutex<E>>,
152    application: A,
153    marshal: Mailbox<S, Standard<B>>,
154    epocher: ES,
155    gates: Gates<<B as Digestible>::Digest, B>,
156
157    build_duration: Timed,
158    proposal_parent_fetch_duration: Timed,
159    ancestor_fetch_duration: Timed,
160}
161
162impl<E, S, A, B, ES> Clone for Deferred<E, S, A, B, ES>
163where
164    E: Rng + Spawner + Metrics + Clock,
165    S: Scheme,
166    A: Application<E>,
167    B: CertifiableBlock,
168    ES: Epocher,
169{
170    fn clone(&self) -> Self {
171        Self {
172            context: self.context.clone(),
173            application: self.application.clone(),
174            marshal: self.marshal.clone(),
175            epocher: self.epocher.clone(),
176            gates: self.gates.clone(),
177            build_duration: self.build_duration.clone(),
178            proposal_parent_fetch_duration: self.proposal_parent_fetch_duration.clone(),
179            ancestor_fetch_duration: self.ancestor_fetch_duration.clone(),
180        }
181    }
182}
183
184impl<E, S, A, B, ES> Deferred<E, S, A, B, ES>
185where
186    E: Rng + Spawner + Metrics + Clock,
187    S: Scheme,
188    A: Application<
189            E,
190            Block = B,
191            SigningScheme = S,
192            Context = Context<B::Digest, S::PublicKey>,
193            Input = (),
194        >,
195    B: CertifiableBlock<Context = <A as Application<E>>::Context>,
196    ES: Epocher,
197{
198    /// Creates a new [`Deferred`] wrapper.
199    pub fn new(context: E, application: A, marshal: Mailbox<S, Standard<B>>, epocher: ES) -> Self {
200        let build_histogram = context.histogram(
201            "build_duration",
202            "Histogram of time taken for the application to build a new block, in seconds",
203            Buckets::LOCAL,
204        );
205        let build_duration = Timed::new(build_histogram);
206        let parent_fetch_histogram = context.histogram(
207            "parent_fetch_duration",
208            "Histogram of time taken to fetch a parent block in propose, in seconds",
209            Buckets::LOCAL,
210        );
211        let proposal_parent_fetch_duration = Timed::new(parent_fetch_histogram);
212        let ancestor_fetch_histogram = context.histogram(
213            "ancestor_fetch_duration",
214            "Histogram of time taken to fetch a block via the ancestry stream, in seconds",
215            Buckets::LOCAL,
216        );
217        let ancestor_fetch_duration = Timed::new(ancestor_fetch_histogram);
218
219        Self {
220            context: Arc::new(TracedAsyncMutex::new("marshal.context", context)),
221            application,
222            marshal,
223            epocher,
224            gates: Gates::new(),
225
226            build_duration,
227            proposal_parent_fetch_duration,
228            ancestor_fetch_duration,
229        }
230    }
231
232    /// Verifies a proposed block's application-level validity.
233    ///
234    /// This method validates that:
235    /// 1. The block's parent digest matches the expected parent
236    /// 2. The block's height is exactly one greater than the parent's height
237    /// 3. The underlying application's verification logic passes
238    ///
239    /// The `parent_request` must be a subscription to the parent named by `context.parent`,
240    /// started by the caller so the parent fetch can overlap work that precedes this call.
241    ///
242    /// Verification is spawned in a background task and returns a receiver that will contain
243    /// the verification result. Valid blocks are reported to the marshal as verified.
244    #[inline]
245    async fn deferred_verify(
246        &mut self,
247        context: <Self as Automaton>::Context,
248        block: Arc<B>,
249        parent_request: oneshot::Receiver<Arc<B>>,
250        stage: Stage,
251    ) -> oneshot::Receiver<GateOutcome> {
252        let marshal = self.marshal.clone();
253        let mut application = self.application.clone();
254        let (mut tx, rx) = oneshot::channel();
255        let ancestor_fetch_duration = self.ancestor_fetch_duration.clone();
256        let runtime_context = self
257            .context
258            .lock()
259            .await
260            .child("deferred_verify")
261            .with_attribute("round", context.round);
262        let span = info_span!(
263            "marshal.deferred.verify.deferred",
264            round = %context.round
265        );
266        runtime_context.spawn(move |runtime_context| {
267            async move {
268                let round = context.round;
269
270                // Start the candidate store immediately: it depends on neither the
271                // parent fetch (which may hit the network) nor the verdict below.
272                // Storing before validation is intentional: these caches provide
273                // candidate availability/recovery, not a validity decision. This
274                // task gates the finalize vote by resolving true only after both
275                // app verification succeeds and the store is durable.
276                let store = stage.store(&marshal, round, Arc::clone(&block));
277                let verify = async {
278                    // Validate the parent we already started fetching.
279                    let parent = match await_and_validate_parent(
280                        context.parent.1,
281                        block.as_ref(),
282                        parent_request,
283                        &mut tx,
284                    )
285                    .await
286                    {
287                        Some(ParentCheck::Valid(parent)) => parent,
288                        Some(ParentCheck::Invalid) => return Some(false),
289                        None => return None,
290                    };
291                    run_app_verify(
292                        runtime_context,
293                        context,
294                        Arc::clone(&block),
295                        parent,
296                        &mut application,
297                        &marshal,
298                        &mut tx,
299                        ancestor_fetch_duration,
300                    )
301                    .await
302                };
303                let (verdict, durable) = futures::join!(verify, store);
304
305                // Publish only when the block is both valid and durable. App-invalid
306                // candidates may already be in the cache from the concurrent store above,
307                // so the gate verdict is the authority for consensus progress.
308                if let Some(application_valid) = gates::resolve(verdict, durable) {
309                    tx.send_lossy(GateOutcome::Ready(application_valid));
310                }
311            }
312            .instrument(span)
313        });
314
315        rx
316    }
317
318    async fn certify_from_embedded_context(
319        &mut self,
320        round: Round,
321        digest: B::Digest,
322    ) -> oneshot::Receiver<bool> {
323        // No in-progress task means we never verified this proposal locally. We can use the
324        // block's embedded context to help complete finalization when Byzantine validators
325        // withhold their finalize votes. If a Byzantine proposer embedded a malicious context,
326        // the f+1 honest validators from the notarizing quorum will verify against the proper
327        // context and reject the mismatch, preventing a 2f+1 finalization quorum.
328        //
329        // We must fetch here rather than only wait for local broadcast delivery. A Byzantine
330        // leader can send a proposal to just f+1 honest validators, collect enough honest
331        // notarize votes to form a notarization, and leave the remaining honest validators
332        // without the block. Those validators need the notarized round to recover the block
333        // and certify; otherwise they can remain stuck if the Byzantine validators stop
334        // participating in the next view.
335        //
336        // Subscribe to the block and verify using its embedded context once available.
337        debug!(
338            ?round,
339            ?digest,
340            "subscribing to block for certification using embedded context"
341        );
342        let block_rx = self
343            .marshal
344            .subscribe_by_digest(digest, DigestFallback::FetchByRound { round });
345        let mut marshaled = self.clone();
346        let epocher = self.epocher.clone();
347        let (mut tx, rx) = oneshot::channel();
348        let context = self
349            .context
350            .lock()
351            .await
352            .child("certify")
353            .with_attribute("round", round);
354        context.spawn(move |_| {
355            async move {
356                let block = select! {
357                    _ = tx.closed() => {
358                        debug!(
359                            reason = "consensus dropped receiver",
360                            "skipping certification"
361                        );
362                        return;
363                    },
364                    result = block_rx => match result {
365                        Ok(block) => block,
366                        Err(_) => {
367                            debug!(
368                                ?digest,
369                                reason = "failed to fetch block for certification",
370                                "skipping certification"
371                            );
372                            return;
373                        }
374                    },
375                };
376
377                // Re-proposal detection for certify path: we don't have the consensus context,
378                // only the block's embedded context from original proposal. Infer re-proposal from:
379                // 1. Block is at epoch boundary (only boundary blocks can be re-proposed)
380                // 2. Certification round's view > embedded context's view (re-proposals retain their
381                //    original embedded context, so a later view indicates the block was re-proposed)
382                // 3. Same epoch (re-proposals don't cross epoch boundaries)
383                let embedded_context = block.context();
384                let is_reproposal = is_inferred_reproposal_at_certify(
385                    &epocher,
386                    block.height(),
387                    embedded_context.round,
388                    round,
389                );
390                if is_reproposal {
391                    // Certifier holds a notarization for this block, so route
392                    // the write to the notarized cache. `certified` is
393                    // idempotent, so crash-recovery double-invocation is safe.
394                    if !marshaled.marshal.certified(round, block).await {
395                        return;
396                    }
397                    tx.send_lossy(true);
398                    return;
399                }
400
401                // Start the parent fetch for the deferred verification below,
402                // which expects a caller-started subscription. Certify does not
403                // carry the consensus context, so the parent round comes from
404                // the block's embedded context. That context is trustworthy
405                // because the digest is notarized and the notarizing quorum's
406                // f+1 honest validators verified it against the consensus
407                // context before voting.
408                let (parent_view, parent_commitment) = embedded_context.parent;
409                let parent_request = marshaled.marshal.subscribe_by_commitment(
410                    parent_commitment,
411                    CommitmentFallback::FetchByRound {
412                        round: Round::new(embedded_context.epoch(), parent_view),
413                    },
414                );
415
416                let verify_rx = marshaled
417                    .deferred_verify(embedded_context, block, parent_request, Stage::Certified)
418                    .await;
419                gates::forward(tx, verify_rx, |result| match result {
420                    GateOutcome::Ready(result) => Some(result),
421                    GateOutcome::Recover => None,
422                })
423                .await;
424            }
425            .instrument(info_span!(
426                "marshal.deferred.certify.embedded",
427                round = %round,
428                digest = %digest
429            ))
430        });
431        rx
432    }
433
434    #[allow(clippy::async_yields_async)]
435    async fn certify_from_existing_task(
436        &mut self,
437        round: Round,
438        digest: B::Digest,
439        task: oneshot::Receiver<GateOutcome>,
440    ) -> oneshot::Receiver<bool> {
441        // `verify()` waits only on local broadcast delivery, so nudge a
442        // round-bound notarized fetch that can unblock the existing waiter
443        // if local broadcast never arrives. For the standard variant, the
444        // digest is also the variant commitment.
445        self.marshal.hint_notarized(round, digest);
446
447        // A completed gate either carries an applicable local verdict or requests
448        // recovery. After an unclean restart the in-memory task is gone, which also
449        // recovers via the embedded-context fetch path.
450        let mut marshaled = self.clone();
451        let (tx, rx) = oneshot::channel();
452        let context = self
453            .context
454            .lock()
455            .await
456            .child("certify_existing")
457            .with_attribute("round", round);
458        context.spawn(move |_| {
459            gates::drive(tx, task, round, digest, move || async move {
460                marshaled.certify_from_embedded_context(round, digest).await
461            })
462            .instrument(info_span!(
463                "marshal.deferred.certify.existing",
464                round = %round,
465                digest = %digest
466            ))
467        });
468        rx
469    }
470}
471
472impl<E, S, A, B, ES> Automaton for Deferred<E, S, A, B, ES>
473where
474    E: Rng + Spawner + Metrics + Clock,
475    S: Scheme,
476    A: Application<
477            E,
478            Block = B,
479            SigningScheme = S,
480            Context = Context<B::Digest, S::PublicKey>,
481            Input = (),
482        >,
483    B: CertifiableBlock<Context = <A as Application<E>>::Context>,
484    ES: Epocher,
485{
486    type Digest = B::Digest;
487    type Context = Context<Self::Digest, S::PublicKey>;
488
489    /// Proposes a new block or re-proposes the epoch boundary block.
490    ///
491    /// This method builds a new block from the underlying application unless the parent block
492    /// is the last block in the current epoch. When at an epoch boundary, it re-proposes the
493    /// boundary block to avoid creating blocks that would be invalidated by the epoch transition.
494    ///
495    /// The proposal operation is spawned in a background task and returns a receiver that will
496    /// contain the proposed block's digest when ready. The block is staged before the digest is
497    /// delivered and handed to marshal when consensus requests the relay broadcast, which
498    /// persists it after the send. The resulting sync handle is awaited only at certification so
499    /// it overlaps consensus voting. The digest does not imply durability on
500    /// its own; [`CertifiableAutomaton::certify`] awaits the registered certification gate before
501    /// the finalize vote.
502    #[allow(clippy::async_yields_async)]
503    #[tracing::instrument(name = "marshal.deferred.propose", level = "info", skip_all, fields(round = %consensus_context.round))]
504    async fn propose(
505        &mut self,
506        consensus_context: Context<Self::Digest, S::PublicKey>,
507    ) -> oneshot::Receiver<Self::Digest> {
508        let marshal = self.marshal.clone();
509        let mut application = self.application.clone();
510        let epocher = self.epocher.clone();
511        let gates = self.gates.clone();
512
513        // Metrics
514        let build_duration = self.build_duration.clone();
515        let proposal_parent_fetch_duration = self.proposal_parent_fetch_duration.clone();
516        let ancestor_fetch_duration = self.ancestor_fetch_duration.clone();
517
518        let (mut tx, rx) = oneshot::channel();
519        let context = self
520            .context
521            .lock()
522            .await
523            .child("propose")
524            .with_attribute("round", consensus_context.round);
525        let span = info_span!(
526            "marshal.deferred.propose.task",
527            round = %consensus_context.round
528        );
529        context.spawn(move |runtime_context| {
530            async move {
531                // On leader recovery, marshal may already hold a verified block
532                // for this round (persisted by a pre-crash propose that reached
533                // its relay broadcast while the notarize vote never reached the
534                // journal).
535                //
536                // The pre-crash digest may already have been broadcast, so
537                // building a fresh block would equivocate. The stored block is
538                // the only proposal we can broadcast for this round.
539                //
540                // The recovered block is safe to reuse only if its embedded
541                // context matches the context simplex just recovered, or if it
542                // is the parent re-proposed at the epoch boundary: that stores the
543                // parent under its original context, whose round is the parent's own.
544                // Otherwise the cached block was built against a different
545                // parent and cannot be broadcast under the current header, so
546                // drop the receiver and let the voter nullify the view via
547                // timeout.
548                let last_in_epoch = epocher
549                    .last(consensus_context.epoch())
550                    .expect("current epoch should exist");
551                if let Some(block) = marshal.get_verified(consensus_context.round).await {
552                    let block_context = block.context();
553                    let digest = block.digest();
554                    let reproposal =
555                        digest == consensus_context.parent.1 && block.height() == last_in_epoch;
556                    if !reproposal && block_context != consensus_context {
557                        debug!(
558                            round = ?consensus_context.round,
559                            ?consensus_context,
560                            ?block_context,
561                            "skipping proposal: cached verified block context no longer matches"
562                        );
563                        return;
564                    }
565                    // Stage the recovered block so the relay broadcast re-sends
566                    // it through the same handshake as a fresh proposal. The
567                    // relay-time persist deduplicates against the pre-crash
568                    // write, with the handle covering the original.
569                    debug!(
570                        round = ?consensus_context.round,
571                        ?digest,
572                        reproposal,
573                        "reusing verified block from marshal on leader recovery"
574                    );
575                    gates
576                        .stage(
577                            consensus_context.round,
578                            digest,
579                            Arc::new(block),
580                            tx,
581                            "recovered block",
582                        )
583                        .await;
584                    return;
585                }
586
587                // The parent for any consensus context is in the same epoch: the
588                // boundary block of the previous epoch is the genesis block of the
589                // current epoch.
590                //
591                // Proposal context carries the certified parent view/commitment but
592                // not the parent height. The parent may be certified above the
593                // finalized tip, so this must stay round-bound until the block is
594                // returned.
595                let (parent_view, parent_commitment) = consensus_context.parent;
596                let parent_request = marshal.subscribe_by_commitment(
597                    parent_commitment,
598                    CommitmentFallback::FetchByRound {
599                        round: Round::new(consensus_context.epoch(), parent_view),
600                    },
601                );
602
603                let parent_timer = proposal_parent_fetch_duration.timer(&runtime_context);
604                let parent = select! {
605                    _ = tx.closed() => {
606                        debug!(reason = "consensus dropped receiver", "skipping proposal");
607                        return;
608                    },
609                    result = parent_request => match result {
610                        Ok(parent) => parent,
611                        Err(_) => {
612                            debug!(
613                                ?parent_commitment,
614                                reason = "failed to fetch parent block",
615                                "skipping proposal"
616                            );
617                            return;
618                        }
619                    },
620                };
621                parent_timer.observe(&runtime_context);
622
623                // Special case: If the parent block is the last block in the epoch,
624                // re-propose it as to not produce any blocks that will be cut out
625                // by the epoch transition.
626                if parent.height() == last_in_epoch {
627                    let digest = parent.digest();
628                    gates
629                        .stage(
630                            consensus_context.round,
631                            digest,
632                            parent,
633                            tx,
634                            "re-proposed boundary block",
635                        )
636                        .await;
637                    return;
638                }
639
640                let ancestor_stream = marshal.ancestor_stream(
641                    Arc::new(runtime_context.child("ancestor_stream")),
642                    [parent],
643                    ancestor_fetch_duration,
644                );
645                let build_request = application
646                    .propose(
647                        (
648                            runtime_context.child("app_propose"),
649                            consensus_context.clone(),
650                        ),
651                        ancestor_stream,
652                        (),
653                    )
654                    .instrument(info_span!(
655                        "marshal.deferred.application.propose",
656                        round = %consensus_context.round,
657                        parent_view = parent_view.traced(),
658                        parent = %parent_commitment
659                    ));
660
661                let build_timer = build_duration.timer(&runtime_context);
662                let built_block = select! {
663                    _ = tx.closed() => {
664                        debug!(reason = "consensus dropped receiver", "skipping proposal");
665                        return;
666                    },
667                    result = build_request => match result {
668                        Some(block) => block,
669                        None => {
670                            debug!(
671                                ?parent_commitment,
672                                reason = "block building failed",
673                                "skipping proposal"
674                            );
675                            return;
676                        }
677                    },
678                };
679                build_timer.observe(&runtime_context);
680
681                let digest = built_block.digest();
682                gates
683                    .stage(
684                        consensus_context.round,
685                        digest,
686                        Arc::new(built_block),
687                        tx,
688                        "proposed block",
689                    )
690                    .await;
691            }
692            .instrument(span)
693        });
694        rx
695    }
696
697    #[allow(clippy::async_yields_async)]
698    #[tracing::instrument(name = "marshal.deferred.verify", level = "info", skip_all, fields(round = %context.round, digest = %digest))]
699    async fn verify(
700        &mut self,
701        context: Context<Self::Digest, S::PublicKey>,
702        digest: Self::Digest,
703    ) -> oneshot::Receiver<bool> {
704        let marshal = self.marshal.clone();
705        let mut marshaled = self.clone();
706        let round = context.round;
707
708        // Verification needs the full block but waits only for local delivery. Certification starts
709        // recovery only when the block is not buffered. If a buffered block is evicted before
710        // verification registers its wait, verification is left with neither the block nor an
711        // active fetch. Register the wait before publishing the gate so it receives the buffered
712        // block or is waiting when recovery delivers it.
713        let block_request = marshal.subscribe_by_digest(digest, DigestFallback::Wait);
714        let (task_tx, task_rx) = oneshot::channel();
715        self.gates.insert(round, digest, task_rx);
716
717        let (mut tx, rx) = oneshot::channel();
718        let runtime_context = self
719            .context
720            .lock()
721            .await
722            .child("optimistic_verify")
723            .with_attribute("round", round);
724        runtime_context.spawn(move |_| {
725            async move {
726                // Start the parent fetch immediately: its commitment and certified
727                // round are known from the consensus context, so it can proceed in
728                // parallel with broadcast delivery of the candidate block.
729                // Reproposals (digest == context.parent.1) skip parent validation
730                // entirely, so they must not fetch: the "parent" is the candidate
731                // itself, and candidate acquisition is deliberately local-only.
732                let parent_request = (digest != context.parent.1).then(|| {
733                    let (parent_view, parent_commitment) = context.parent;
734                    marshal.subscribe_by_commitment(
735                        parent_commitment,
736                        CommitmentFallback::FetchByRound {
737                            round: Round::new(context.epoch(), parent_view),
738                        },
739                    )
740                });
741
742                // Stop waiting for the block if consensus drops the verification request.
743                let block = select! {
744                    _ = tx.closed() => {
745                        debug!(
746                            reason = "consensus dropped receiver",
747                            "skipping optimistic verification"
748                        );
749                        return;
750                    },
751                    result = block_request => match result {
752                        Ok(block) => block,
753                        Err(_) => {
754                            debug!(
755                                ?digest,
756                                reason = "failed to fetch block for optimistic verification",
757                                "skipping optimistic verification"
758                            );
759                            return;
760                        }
761                    },
762                };
763
764                // Shared pre-checks enforce:
765                // - Block epoch membership.
766                // - Re-proposal detection via `digest == context.parent.1`.
767                //
768                // Re-proposals return early and skip normal parent/height checks
769                // because consensus settles their validity when certifying the view
770                // that first carried the block, and parent-child checks would fail by
771                // construction when parent == block.
772                let Some(decision) = precheck_epoch_and_reproposal(
773                    &marshaled.epocher,
774                    &marshal,
775                    &context,
776                    digest,
777                    block,
778                )
779                .await
780                else {
781                    return;
782                };
783                let block = match decision {
784                    Decision::Complete(valid) => {
785                        // `Complete` means either immediate rejection or successful
786                        // re-proposal handling with no further ancestry validation.
787                        //
788                        // A rejection is safe to publish as a gate verdict because the
789                        // precheck depends only on the block's height and the gate key's
790                        // epoch, never on the declared parent. A conflicting header for
791                        // the same `(round, digest)` cannot produce an honest
792                        // notarization: reading the digest as a re-proposal requires it
793                        // to be the recorded payload of an earlier view, so its embedded
794                        // context fails the mismatch check under any normal header.
795                        task_tx.send_lossy(GateOutcome::Ready(valid));
796                        tx.send_lossy(valid);
797                        return;
798                    }
799                    Decision::Continue(block) => block,
800                };
801
802                // `Continue` implies a non-reproposal, so the parent subscription
803                // was started above.
804                let parent_request =
805                    parent_request.expect("non-reproposal has a parent subscription");
806
807                // Before casting a notarize vote, ensure the block's embedded context matches
808                // the consensus context.
809                //
810                // This is a critical step - the notarize quorum is guaranteed to have at least
811                // f+1 honest validators who will verify against this context, preventing a Byzantine
812                // proposer from embedding a malicious context. The other f honest validators who did
813                // not vote will later use the block-embedded context to help finalize if Byzantine
814                // validators withhold their finalize votes.
815                if block.context() != context {
816                    debug!(
817                        ?context,
818                        block_context = ?block.context(),
819                        "block-embedded context does not match consensus context during optimistic verification"
820                    );
821                    task_tx.send_lossy(GateOutcome::Recover);
822                    tx.send_lossy(false);
823                    return;
824                }
825
826                // Optimistic verify returns immediately; the deferred_verify task
827                // runs in the background and forwards its final verdict to
828                // `task_tx` so `certify` observes the same result via the
829                // synchronously-registered `task_rx`.
830                //
831                // Once the optimistic verdict is delivered, the certification
832                // gate owns the deferred work. Nullification keeps that gate
833                // alive, while finalization drops it.
834                let deferred_rx = marshaled
835                    .deferred_verify(context, block, parent_request, Stage::Verified)
836                    .await;
837                tx.send_lossy(true);
838                gates::forward(task_tx, deferred_rx, Some).await;
839            }
840            .instrument(info_span!(
841                "marshal.deferred.verify.optimistic",
842                round = %round,
843                digest = %digest
844            ))
845        });
846        rx
847    }
848}
849
850impl<E, S, A, B, ES> CertifiableAutomaton for Deferred<E, S, A, B, ES>
851where
852    E: Rng + Spawner + Metrics + Clock,
853    S: Scheme,
854    A: Application<
855            E,
856            Block = B,
857            SigningScheme = S,
858            Context = Context<B::Digest, S::PublicKey>,
859            Input = (),
860        >,
861    B: CertifiableBlock<Context = <A as Application<E>>::Context>,
862    ES: Epocher,
863{
864    #[allow(clippy::async_yields_async)]
865    #[tracing::instrument(name = "marshal.deferred.certify", level = "info", skip_all, fields(round = %round, digest = %digest))]
866    async fn certify(&mut self, round: Round, digest: Self::Digest) -> oneshot::Receiver<bool> {
867        self.gates.flush_unrelayed(&self.marshal, round, digest);
868
869        // Attempt to retrieve the existing certification gate task for this round/digest.
870        let task = self.gates.take(round, digest);
871        if let Some(task) = task {
872            return self.certify_from_existing_task(round, digest, task).await;
873        }
874
875        self.certify_from_embedded_context(round, digest).await
876    }
877}
878
879impl<E, S, A, B, ES> Relay for Deferred<E, S, A, B, ES>
880where
881    E: Rng + Spawner + Metrics + Clock,
882    S: Scheme,
883    A: Application<E, Block = B, Context = Context<B::Digest, S::PublicKey>>,
884    B: CertifiableBlock<Context = <A as Application<E>>::Context>,
885    ES: Epocher,
886{
887    type Digest = B::Digest;
888    type PublicKey = S::PublicKey;
889    type Plan = Plan<S::PublicKey>;
890
891    fn broadcast(&mut self, commitment: Self::Digest, plan: Plan<S::PublicKey>) -> Feedback {
892        relay::broadcast(&self.gates, &self.marshal, commitment, plan)
893    }
894}
895
896impl<E, S, A, B, ES> Reporter for Deferred<E, S, A, B, ES>
897where
898    E: Rng + Spawner + Metrics + Clock,
899    S: Scheme,
900    A: Application<E, Block = B, Context = Context<B::Digest, S::PublicKey>>
901        + Reporter<Activity = Update<B>>,
902    B: CertifiableBlock<Context = <A as Application<E>>::Context>,
903    ES: Epocher,
904{
905    type Activity = A::Activity;
906
907    /// Relays a report to the underlying [`Application`] and cleans up old certification gate tasks.
908    fn report(&mut self, update: Self::Activity) -> Feedback {
909        // Clean up certification gate tasks for rounds <= the finalized round.
910        if let Update::Tip(round, _, _) = &update {
911            self.gates.retain_after(round);
912        }
913        self.application.report(update)
914    }
915}
916
917#[cfg(test)]
918mod tests {
919    use super::Deferred;
920    use crate::{
921        Automaton, CertifiableAutomaton, Relay,
922        marshal::mocks::{
923            harness::{
924                B, BLOCKS_PER_EPOCH, Ctx, NAMESPACE, NUM_VALIDATORS, S, StandardHarness,
925                TestHarness, V, default_leader, make_raw_block, setup_network_with_participants,
926            },
927            verifying::{GatedVerifyingApp, MockVerifyingApp},
928        },
929        simplex::{Plan, scheme::bls12381_threshold::vrf as bls12381_threshold_vrf},
930        types::{Epoch, Epocher, FixedEpocher, Height, Round, View},
931    };
932    use commonware_broadcast::Broadcaster;
933    use commonware_cryptography::{
934        Digestible, Hasher as _,
935        certificate::{ConstantProvider, mocks::Fixture},
936        sha256::Sha256,
937    };
938    use commonware_macros::{select, test_traced};
939    use commonware_runtime::{Clock, Runner, Supervisor as _, deterministic};
940    use commonware_utils::{NZUsize, channel::fallible::OneshotExt};
941    use std::time::Duration;
942
943    #[test_traced("INFO")]
944    fn test_certify_lower_view_after_higher_view() {
945        let runner = deterministic::Runner::timed(Duration::from_secs(60));
946        runner.start(|mut context| async move {
947            let Fixture {
948                participants,
949                schemes,
950                ..
951            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
952            let mut oracle = setup_network_with_participants(
953                context.child("network"),
954                NZUsize!(1),
955                participants.clone(),
956            )
957            .await;
958
959            let me = participants[0].clone();
960
961            let setup = StandardHarness::setup_validator(
962                context.child("validator").with_attribute("index", 0),
963                &mut oracle,
964                me.clone(),
965                ConstantProvider::new(schemes[0].clone()),
966            )
967            .await;
968            let marshal = setup.mailbox;
969
970            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
971            let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
972
973            let mut marshaled = Deferred::new(
974                context.child("deferred"),
975                mock_app,
976                marshal.clone(),
977                FixedEpocher::new(BLOCKS_PER_EPOCH),
978            );
979
980            // Create parent block at height 1
981            let parent = make_raw_block(genesis.digest(), Height::new(1), 100);
982            let parent_digest = parent.digest();
983
984            assert!(
985                marshal
986                    .verified(Round::new(Epoch::new(0), View::new(1)), parent.clone())
987                    .await
988            );
989
990            // Block A at view 5 (height 2)
991            let round_a = Round::new(Epoch::new(0), View::new(5));
992            let context_a = Ctx {
993                round: round_a,
994                leader: me.clone(),
995                parent: (View::new(1), parent_digest),
996            };
997            let block_a = B::new::<Sha256>(context_a.clone(), parent_digest, Height::new(2), 200);
998            let commitment_a = StandardHarness::commitment(&block_a);
999            assert!(marshal.verified(round_a, block_a.clone()).await);
1000
1001            // Block B at view 10 (height 2, different block same height)
1002            let round_b = Round::new(Epoch::new(0), View::new(10));
1003            let context_b = Ctx {
1004                round: round_b,
1005                leader: me.clone(),
1006                parent: (View::new(1), parent_digest),
1007            };
1008            let block_b = B::new::<Sha256>(context_b.clone(), parent_digest, Height::new(2), 300);
1009            let commitment_b = StandardHarness::commitment(&block_b);
1010            assert!(marshal.verified(round_b, block_b.clone()).await);
1011
1012            context.sleep(Duration::from_millis(10)).await;
1013
1014            // Step 1: Verify block A at view 5
1015            let _ = marshaled.verify(context_a, commitment_a).await.await;
1016
1017            // Step 2: Verify block B at view 10
1018            let _ = marshaled.verify(context_b, commitment_b).await.await;
1019
1020            // Step 3: Certify block B at view 10 FIRST
1021            let certify_b = marshaled.certify(round_b, commitment_b).await;
1022            assert!(
1023                certify_b.await.unwrap(),
1024                "Block B certification should succeed"
1025            );
1026
1027            // Step 4: Certify block A at view 5 - should succeed
1028            let certify_a = marshaled.certify(round_a, commitment_a).await;
1029
1030            select! {
1031                result = certify_a => {
1032                    assert!(result.unwrap(), "Block A certification should succeed");
1033                },
1034                _ = context.sleep(Duration::from_secs(5)) => {
1035                    panic!("Block A certification timed out");
1036                },
1037            }
1038        })
1039    }
1040
1041    #[test_traced("WARN")]
1042    fn test_marshaled_rejects_unsupported_epoch() {
1043        #[derive(Clone)]
1044        struct LimitedEpocher {
1045            inner: FixedEpocher,
1046            max_epoch: u64,
1047        }
1048
1049        impl Epocher for LimitedEpocher {
1050            fn containing(&self, height: Height) -> Option<crate::types::EpochInfo> {
1051                let bounds = self.inner.containing(height)?;
1052                if bounds.epoch().get() > self.max_epoch {
1053                    None
1054                } else {
1055                    Some(bounds)
1056                }
1057            }
1058
1059            fn first(&self, epoch: Epoch) -> Option<Height> {
1060                if epoch.get() > self.max_epoch {
1061                    None
1062                } else {
1063                    self.inner.first(epoch)
1064                }
1065            }
1066
1067            fn last(&self, epoch: Epoch) -> Option<Height> {
1068                if epoch.get() > self.max_epoch {
1069                    None
1070                } else {
1071                    self.inner.last(epoch)
1072                }
1073            }
1074        }
1075
1076        let runner = deterministic::Runner::timed(Duration::from_secs(60));
1077        runner.start(|mut context| async move {
1078            let Fixture {
1079                participants,
1080                schemes,
1081                ..
1082            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1083            let mut oracle = setup_network_with_participants(
1084                context.child("network"),
1085                NZUsize!(1),
1086                participants.clone(),
1087            )
1088            .await;
1089
1090            let me = participants[0].clone();
1091
1092            let setup = StandardHarness::setup_validator(
1093                context.child("validator").with_attribute("index", 0),
1094                &mut oracle,
1095                me.clone(),
1096                ConstantProvider::new(schemes[0].clone()),
1097            )
1098            .await;
1099            let marshal = setup.mailbox;
1100
1101            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1102            let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1103            let limited_epocher = LimitedEpocher {
1104                inner: FixedEpocher::new(BLOCKS_PER_EPOCH),
1105                max_epoch: 0,
1106            };
1107
1108            let mut marshaled = Deferred::new(
1109                context.child("deferred"),
1110                mock_app,
1111                marshal.clone(),
1112                limited_epocher,
1113            );
1114
1115            // Create a parent block at height 19 (last block in epoch 0, which is supported)
1116            let parent_ctx = Ctx {
1117                round: Round::new(Epoch::zero(), View::new(19)),
1118                leader: default_leader(),
1119                parent: (View::zero(), genesis.digest()),
1120            };
1121            let parent =
1122                B::new::<Sha256>(parent_ctx.clone(), genesis.digest(), Height::new(19), 1000);
1123            let parent_digest = parent.digest();
1124
1125            assert!(
1126                marshal
1127                    .clone()
1128                    .verified(Round::new(Epoch::zero(), View::new(19)), parent.clone())
1129                    .await
1130            );
1131
1132            // Create a block at height 20 (first block in epoch 1, which is NOT supported)
1133            let unsupported_round = Round::new(Epoch::new(1), View::new(20));
1134            let unsupported_context = Ctx {
1135                round: unsupported_round,
1136                leader: me.clone(),
1137                parent: (View::new(19), parent_digest),
1138            };
1139            let block = B::new::<Sha256>(
1140                unsupported_context.clone(),
1141                parent_digest,
1142                Height::new(20),
1143                2000,
1144            );
1145            let block_commitment = StandardHarness::commitment(&block);
1146
1147            assert!(
1148                marshal
1149                    .clone()
1150                    .verified(unsupported_round, block.clone())
1151                    .await
1152            );
1153
1154            context.sleep(Duration::from_millis(10)).await;
1155
1156            // Call verify and wait for the result (verify returns optimistic result,
1157            // but also spawns deferred verification)
1158            let verify_result = marshaled
1159                .verify(unsupported_context, block_commitment)
1160                .await;
1161
1162            // Wait for optimistic verify to complete so the certification gate task is registered
1163            let optimistic_result = verify_result.await;
1164
1165            // The optimistic verify should return false because the block is in an unsupported epoch
1166            assert!(
1167                !optimistic_result.unwrap(),
1168                "Optimistic verify should reject block in unsupported epoch"
1169            );
1170        })
1171    }
1172
1173    /// Test that marshaled rejects blocks when consensus context doesn't match block's embedded context.
1174    ///
1175    /// This tests that when verify() is called with a context that doesn't match what's embedded
1176    /// in the block, the verification should fail. A Byzantine proposer could broadcast a block
1177    /// with one embedded context but consensus could call verify() with a different context.
1178    #[test_traced("WARN")]
1179    fn test_marshaled_rejects_mismatched_context() {
1180        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1181        runner.start(|mut context| async move {
1182            let Fixture {
1183                participants,
1184                schemes,
1185                ..
1186            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1187            let mut oracle = setup_network_with_participants(
1188                context.child("network"),
1189                NZUsize!(1),
1190                participants.clone(),
1191            )
1192            .await;
1193
1194            let me = participants[0].clone();
1195
1196            let setup = StandardHarness::setup_validator(
1197                context.child("validator").with_attribute("index", 0),
1198                &mut oracle,
1199                me.clone(),
1200                ConstantProvider::new(schemes[0].clone()),
1201            )
1202            .await;
1203            let marshal = setup.mailbox;
1204
1205            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1206            let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1207
1208            let mut marshaled = Deferred::new(
1209                context.child("deferred"),
1210                mock_app,
1211                marshal.clone(),
1212                FixedEpocher::new(BLOCKS_PER_EPOCH),
1213            );
1214
1215            // Create parent block at height 1 so the commitment is well-formed.
1216            let parent_ctx = Ctx {
1217                round: Round::new(Epoch::zero(), View::new(1)),
1218                leader: default_leader(),
1219                parent: (View::zero(), genesis.digest()),
1220            };
1221            let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
1222            let parent_commitment = StandardHarness::commitment(&parent);
1223
1224            assert!(
1225                marshal
1226                    .clone()
1227                    .verified(Round::new(Epoch::zero(), View::new(1)), parent.clone())
1228                    .await
1229            );
1230
1231            // Build a block with context A (embedded in the block).
1232            let round_a = Round::new(Epoch::zero(), View::new(2));
1233            let context_a = Ctx {
1234                round: round_a,
1235                leader: me.clone(),
1236                parent: (View::new(1), parent_commitment),
1237            };
1238            let block_a = B::new::<Sha256>(context_a, parent.digest(), Height::new(2), 200);
1239            let commitment_a = StandardHarness::commitment(&block_a);
1240            assert!(marshal.verified(round_a, block_a).await);
1241
1242            context.sleep(Duration::from_millis(10)).await;
1243
1244            // Verify using a different consensus context B (hash mismatch).
1245            let round_b = Round::new(Epoch::zero(), View::new(3));
1246            let context_b = Ctx {
1247                round: round_b,
1248                leader: participants[1].clone(),
1249                parent: (View::new(1), parent_commitment),
1250            };
1251
1252            let verify_rx = marshaled.verify(context_b, commitment_a).await;
1253            select! {
1254                result = verify_rx => {
1255                    assert!(
1256                        !result.unwrap(),
1257                        "mismatched context hash should be rejected"
1258                    );
1259                },
1260                _ = context.sleep(Duration::from_secs(5)) => {
1261                    panic!("verify should reject mismatched context hash promptly");
1262                },
1263            }
1264        })
1265    }
1266
1267    /// Dropping the optimistic verify receiver before the block is available can close the
1268    /// synchronously-registered certification gate task. `certify` must recover through the
1269    /// embedded-context path instead of returning the closed task to consensus.
1270    #[test_traced("WARN")]
1271    fn test_deferred_certify_recovers_after_verify_receiver_drop() {
1272        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1273        runner.start(|mut context| async move {
1274            let Fixture {
1275                participants,
1276                schemes,
1277                ..
1278            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1279            let mut oracle = setup_network_with_participants(
1280                context.child("network"),
1281                NZUsize!(1),
1282                participants.clone(),
1283            )
1284            .await;
1285
1286            let me = participants[0].clone();
1287            let setup = StandardHarness::setup_validator(
1288                context.child("validator").with_attribute("index", 0),
1289                &mut oracle,
1290                me.clone(),
1291                ConstantProvider::new(schemes[0].clone()),
1292            )
1293            .await;
1294            let marshal = setup.mailbox;
1295
1296            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1297            let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1298            let mut marshaled = Deferred::new(
1299                context.child("deferred"),
1300                mock_app,
1301                marshal.clone(),
1302                FixedEpocher::new(BLOCKS_PER_EPOCH),
1303            );
1304
1305            let round = Round::new(Epoch::zero(), View::new(1));
1306            let block_context = Ctx {
1307                round,
1308                leader: me,
1309                parent: (View::zero(), genesis.digest()),
1310            };
1311            let block =
1312                B::new::<Sha256>(block_context.clone(), genesis.digest(), Height::new(1), 100);
1313            let digest = block.digest();
1314
1315            let verify_rx = marshaled.verify(block_context, digest).await;
1316            drop(verify_rx);
1317
1318            // Give the optimistic task a chance to observe the dropped receiver while its
1319            // block subscription is still pending.
1320            context.sleep(Duration::from_millis(10)).await;
1321
1322            assert!(marshal.verified(round, block).await);
1323            let certify_rx = marshaled.certify(round, digest).await;
1324            select! {
1325                result = certify_rx => {
1326                    assert!(
1327                        result.expect("certify result missing"),
1328                        "certify should recover after verify receiver drop"
1329                    );
1330                },
1331                _ = context.sleep(Duration::from_secs(5)) => {
1332                    panic!("certify should recover promptly after verify drop");
1333                },
1334            }
1335        });
1336    }
1337
1338    /// The store request runs concurrently with `app.verify`, not after it: while
1339    /// gated application verification is still blocked, the block has already
1340    /// reached marshal and is locally queryable even though the sync handle may
1341    /// still be pending. Releasing verification then lets certification await
1342    /// the registered certification gate. Separate restart tests cover durable
1343    /// recovery after certification.
1344    #[test_traced("WARN")]
1345    fn test_deferred_store_overlaps_app_verify() {
1346        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1347        runner.start(|mut context| async move {
1348            let Fixture {
1349                participants,
1350                schemes,
1351                ..
1352            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1353            let mut oracle = setup_network_with_participants(
1354                context.child("network"),
1355                NZUsize!(1),
1356                participants.clone(),
1357            )
1358            .await;
1359
1360            let me = participants[0].clone();
1361
1362            let setup = StandardHarness::setup_validator(
1363                context.child("validator").with_attribute("index", 0),
1364                &mut oracle,
1365                me.clone(),
1366                ConstantProvider::new(schemes[0].clone()),
1367            )
1368            .await;
1369            let marshal = setup.mailbox;
1370            let buffer = setup.extra;
1371
1372            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1373            let (mock_app, verify_started, release_verify): (GatedVerifyingApp<B, S>, _, _) =
1374                GatedVerifyingApp::new();
1375            let mut marshaled = Deferred::new(
1376                context.child("deferred"),
1377                mock_app,
1378                marshal.clone(),
1379                FixedEpocher::new(BLOCKS_PER_EPOCH),
1380            );
1381
1382            // Seed parent and child via the buffer (in-memory only) so
1383            // `deferred_verify` can fetch them without going through the
1384            // persisted marshal path.
1385            let parent = make_raw_block(genesis.digest(), Height::new(1), 100);
1386            let parent_digest = parent.digest();
1387
1388            let child_round = Round::new(Epoch::zero(), View::new(2));
1389            let child_ctx = Ctx {
1390                round: child_round,
1391                leader: me,
1392                parent: (View::new(1), parent_digest),
1393            };
1394            let child = B::new::<Sha256>(child_ctx.clone(), parent_digest, Height::new(2), 200);
1395            let child_digest = child.digest();
1396
1397            assert!(
1398                buffer
1399                    .broadcast(commonware_p2p::Recipients::Some(vec![]), parent)
1400                    .accepted(),
1401                "buffer broadcast for parent should be accepted"
1402            );
1403            assert!(
1404                buffer
1405                    .broadcast(commonware_p2p::Recipients::Some(vec![]), child)
1406                    .accepted(),
1407                "buffer broadcast for child should be accepted"
1408            );
1409
1410            // Kick off the optimistic verify, which spawns `deferred_verify`. Its gated
1411            // `app.verify` blocks until we release it.
1412            let optimistic_rx = marshaled.verify(child_ctx, child_digest).await;
1413            assert!(
1414                optimistic_rx
1415                    .await
1416                    .expect("optimistic verify should resolve"),
1417                "optimistic verify should accept the available block"
1418            );
1419
1420            // Application verification is now blocked. The store request runs concurrently
1421            // with it, so the block is locally queryable even though verification has not
1422            // returned and the sync handle may still be pending.
1423            verify_started
1424                .await
1425                .expect("verify should reach the gated application");
1426            assert!(
1427                marshal.get_block(&child_digest).await.is_some(),
1428                "the store request runs concurrently with app.verify, so the block is locally queryable while verification is still gated"
1429            );
1430
1431            // Releasing verification lets certification succeed (valid and durable).
1432            release_verify.send_lossy(());
1433            let certify_rx = marshaled.certify(child_round, child_digest).await;
1434            select! {
1435                result = certify_rx => {
1436                    assert!(
1437                        result.expect("certify result missing"),
1438                        "certify should succeed once verification passes"
1439                    );
1440                },
1441                _ = context.sleep(Duration::from_secs(5)) => {
1442                    panic!("certify should resolve after verification is released");
1443                },
1444            }
1445        });
1446    }
1447
1448    /// Regression: when marshal holds a verified block for a round from a
1449    /// pre-crash propose, a restarted leader's `propose` must return that
1450    /// block's digest instead of asking the application to build afresh.
1451    /// The recovered proposal must also be staged for the relay, so the
1452    /// broadcast re-sends it and certification resolves through the
1453    /// deduplicated re-persist. The inline variant skips the view instead
1454    /// (see `inline::tests::test_propose_skips_when_verified_block_exists_on_restart`).
1455    #[test_traced("WARN")]
1456    fn test_propose_reuses_verified_block_on_restart() {
1457        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1458        runner.start(|mut context| async move {
1459            let Fixture {
1460                participants,
1461                schemes,
1462                ..
1463            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1464            let mut oracle = setup_network_with_participants(
1465                context.child("network"),
1466                NZUsize!(1),
1467                participants.clone(),
1468            )
1469            .await;
1470
1471            let me = participants[0].clone();
1472            let setup = StandardHarness::setup_validator(
1473                context.child("validator").with_attribute("index", 0),
1474                &mut oracle,
1475                me.clone(),
1476                ConstantProvider::new(schemes[0].clone()),
1477            )
1478            .await;
1479            let marshal = setup.mailbox;
1480
1481            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1482            let round = Round::new(Epoch::zero(), View::new(1));
1483            let ctx = Ctx {
1484                round,
1485                leader: me.clone(),
1486                parent: (View::zero(), genesis.digest()),
1487            };
1488            let block_a = B::new::<Sha256>(ctx.clone(), genesis.digest(), Height::new(1), 100);
1489            let digest_a = block_a.digest();
1490            assert!(marshal.verified(round, block_a.clone()).await);
1491
1492            // The app cannot build (`propose` returns None) and its
1493            // verification never completes, so the assertions below hold
1494            // only if the stored block is reused as-is and certification
1495            // resolves through the durability gate registered by the
1496            // recovery staging.
1497            let (mock_app, verify_started, _release_verify): (GatedVerifyingApp<B, S>, _, _) =
1498                GatedVerifyingApp::new();
1499            let mut marshaled = Deferred::new(
1500                context.child("deferred"),
1501                mock_app,
1502                marshal.clone(),
1503                FixedEpocher::new(BLOCKS_PER_EPOCH),
1504            );
1505
1506            let digest_rx = marshaled.propose(ctx).await;
1507            let digest = digest_rx.await.expect("propose must return a digest");
1508            assert_eq!(
1509                digest, digest_a,
1510                "propose must reuse the block marshal already persisted for this round"
1511            );
1512
1513            // The relay broadcast must find the recovered proposal staged and
1514            // re-persist it (a dedup no-op whose handle covers the pre-crash
1515            // write), resolving the certification gate registered by the
1516            // recovery path.
1517            let _ = marshaled.broadcast(digest, Plan::Propose { round });
1518            let certify_rx = marshaled.certify(round, digest).await;
1519            select! {
1520                result = certify_rx => {
1521                    assert!(
1522                        result.expect("certify result missing"),
1523                        "recovered proposal must certify through the relay handshake"
1524                    );
1525                },
1526                _ = verify_started => {
1527                    panic!("certifying a recovered proposal must not run app verification");
1528                },
1529            }
1530        });
1531    }
1532
1533    /// Regression: a boundary re-proposal stores the parent block itself at
1534    /// the re-proposal round, under the parent's original embedded context.
1535    /// A leader that crashes after that relay broadcast must recognize the
1536    /// cached parent as the re-proposal on restart and propose it again,
1537    /// rather than skipping the round because its embedded context names an
1538    /// older round.
1539    #[test_traced("WARN")]
1540    fn test_propose_reuses_reproposed_boundary_block_on_restart() {
1541        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1542        runner.start(|mut context| async move {
1543            let Fixture {
1544                participants,
1545                schemes,
1546                ..
1547            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1548            let mut oracle = setup_network_with_participants(
1549                context.child("network"),
1550                NZUsize!(1),
1551                participants.clone(),
1552            )
1553            .await;
1554
1555            let me = participants[0].clone();
1556            let setup = StandardHarness::setup_validator(
1557                context.child("validator").with_attribute("index", 0),
1558                &mut oracle,
1559                me.clone(),
1560                ConstantProvider::new(schemes[0].clone()),
1561            )
1562            .await;
1563            let marshal = setup.mailbox;
1564
1565            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1566
1567            // Seed the boundary block at the re-proposal round, where the
1568            // pre-crash relay broadcast of the re-proposal persisted it.
1569            let boundary_height = Height::new(BLOCKS_PER_EPOCH.get() - 1);
1570            let boundary_round = Round::new(Epoch::zero(), View::new(boundary_height.get()));
1571            let boundary_block = B::new::<Sha256>(
1572                Ctx {
1573                    round: boundary_round,
1574                    leader: default_leader(),
1575                    parent: (View::zero(), genesis.digest()),
1576                },
1577                genesis.digest(),
1578                boundary_height,
1579                1900,
1580            );
1581            let boundary_digest = boundary_block.digest();
1582            let round = Round::new(Epoch::zero(), View::new(boundary_height.get() + 1));
1583            assert!(marshal.verified(round, boundary_block).await);
1584
1585            let ctx = Ctx {
1586                round,
1587                leader: me.clone(),
1588                parent: (View::new(boundary_height.get()), boundary_digest),
1589            };
1590
1591            // The app cannot build and its verification never completes, so
1592            // the assertions below hold only if the cached parent is
1593            // re-proposed as-is.
1594            let (mock_app, verify_started, _release_verify): (GatedVerifyingApp<B, S>, _, _) =
1595                GatedVerifyingApp::new();
1596            let mut marshaled = Deferred::new(
1597                context.child("deferred"),
1598                mock_app,
1599                marshal.clone(),
1600                FixedEpocher::new(BLOCKS_PER_EPOCH),
1601            );
1602
1603            let digest_rx = marshaled.propose(ctx).await;
1604            let digest = digest_rx.await.expect("propose must return a digest");
1605            assert_eq!(
1606                digest, boundary_digest,
1607                "propose must re-propose the boundary block marshal already persisted for this round"
1608            );
1609
1610            let _ = marshaled.broadcast(digest, Plan::Propose { round });
1611            let certify_rx = marshaled.certify(round, digest).await;
1612            select! {
1613                result = certify_rx => {
1614                    assert!(
1615                        result.expect("certify result missing"),
1616                        "re-proposed boundary block must certify through the relay handshake"
1617                    );
1618                },
1619                _ = verify_started => {
1620                    panic!("certifying a re-proposed boundary block must not run app verification");
1621                },
1622            }
1623        });
1624    }
1625
1626    /// Regression: if a pre-crash leader persisted a verified block for a
1627    /// round but the simplex `Notarize` never reached the journal, replay
1628    /// can recover a `consensus_context` whose parent differs from the one
1629    /// the cached block was built against (e.g. a late certification of an
1630    /// older view changes the parent selected by `State::find_parent`).
1631    /// In that case the restarted leader must not broadcast the stale
1632    /// cached block; it must drop the receiver so the voter nullifies the
1633    /// view via `MissingProposal`.
1634    #[test_traced("WARN")]
1635    fn test_propose_skips_when_verified_block_context_changed() {
1636        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1637        runner.start(|mut context| async move {
1638            let Fixture {
1639                participants,
1640                schemes,
1641                ..
1642            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1643            let mut oracle = setup_network_with_participants(
1644                context.child("network"),
1645                NZUsize!(1),
1646                participants.clone(),
1647            )
1648            .await;
1649
1650            let me = participants[0].clone();
1651            let setup = StandardHarness::setup_validator(
1652                context.child("validator").with_attribute("index", 0),
1653                &mut oracle,
1654                me.clone(),
1655                ConstantProvider::new(schemes[0].clone()),
1656            )
1657            .await;
1658            let marshal = setup.mailbox;
1659
1660            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1661
1662            // Stash a stale block built against genesis as its parent at round V=2.
1663            let round = Round::new(Epoch::zero(), View::new(2));
1664            let stale_ctx = Ctx {
1665                round,
1666                leader: me.clone(),
1667                parent: (View::zero(), genesis.digest()),
1668            };
1669            let stale_block = B::new::<Sha256>(stale_ctx, genesis.digest(), Height::new(1), 100);
1670            assert!(marshal.verified(round, stale_block).await);
1671
1672            // Simulate a replay where parent selection now points to a
1673            // different parent view than the cached block was built for.
1674            let new_parent_digest = Sha256::hash(&[b"late-certified-parent"]);
1675            let new_ctx = Ctx {
1676                round,
1677                leader: me.clone(),
1678                parent: (View::new(1), new_parent_digest),
1679            };
1680
1681            let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1682            let mut marshaled = Deferred::new(
1683                context.child("deferred"),
1684                mock_app,
1685                marshal.clone(),
1686                FixedEpocher::new(BLOCKS_PER_EPOCH),
1687            );
1688
1689            let digest_rx = marshaled.propose(new_ctx).await;
1690            assert!(
1691                digest_rx.await.is_err(),
1692                "propose must drop the receiver when the cached block's context no longer matches"
1693            );
1694        });
1695    }
1696
1697    /// Regression: in deferred mode `propose` registers a certification gate that
1698    /// `certify` awaits. After the leader certifies its own proposal, the block must be
1699    /// durably recoverable across an unclean restart. This is the >= f+1 guarantee
1700    /// for the leader's own block.
1701    #[test_traced("WARN")]
1702    fn test_deferred_propose_then_certify_persists_block() {
1703        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1704        runner.start(|mut context| async move {
1705            let Fixture {
1706                participants,
1707                schemes,
1708                ..
1709            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1710            let mut oracle = setup_network_with_participants(
1711                context.child("network"),
1712                NZUsize!(1),
1713                participants.clone(),
1714            )
1715            .await;
1716
1717            let me = participants[0].clone();
1718            let setup = StandardHarness::setup_validator(
1719                context.child("validator").with_attribute("index", 0),
1720                &mut oracle,
1721                me.clone(),
1722                ConstantProvider::new(schemes[0].clone()),
1723            )
1724            .await;
1725            let marshal = setup.mailbox;
1726            let actor_handle = setup.actor_handle;
1727
1728            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1729
1730            // Seed the parent at its round so `propose` can fetch it locally.
1731            let parent_round = Round::new(Epoch::zero(), View::new(1));
1732            let parent_ctx = Ctx {
1733                round: parent_round,
1734                leader: default_leader(),
1735                parent: (View::zero(), genesis.digest()),
1736            };
1737            let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
1738            let parent_digest = parent.digest();
1739            assert!(marshal.verified(parent_round, parent).await);
1740
1741            // The leader builds the child via `app.propose`.
1742            let round = Round::new(Epoch::zero(), View::new(2));
1743            let ctx = Ctx {
1744                round,
1745                leader: me.clone(),
1746                parent: (View::new(1), parent_digest),
1747            };
1748            let child = B::new::<Sha256>(ctx.clone(), parent_digest, Height::new(2), 200);
1749            let child_digest = child.digest();
1750            let mock_app: MockVerifyingApp<B, S> =
1751                MockVerifyingApp::new().with_propose_result(child);
1752            let mut marshaled = Deferred::new(
1753                context.child("deferred"),
1754                mock_app,
1755                marshal.clone(),
1756                FixedEpocher::new(BLOCKS_PER_EPOCH),
1757            );
1758
1759            let digest = marshaled
1760                .propose(ctx)
1761                .await
1762                .await
1763                .expect("propose must return a digest");
1764            assert_eq!(
1765                digest, child_digest,
1766                "propose must return the built block's digest"
1767            );
1768
1769            // The leader certifies its own proposal; this awaits the deferred propose sync handle.
1770            assert!(
1771                marshaled
1772                    .certify(round, child_digest)
1773                    .await
1774                    .await
1775                    .expect("certify result missing"),
1776                "certify must succeed for the leader's own proposal"
1777            );
1778
1779            // After certify, the block must be durable across an unclean restart.
1780            actor_handle.abort();
1781            drop(marshaled);
1782            drop(marshal);
1783
1784            let setup2 = StandardHarness::setup_validator(
1785                context
1786                    .child("validator_restart")
1787                    .with_attribute("index", 0),
1788                &mut oracle,
1789                me,
1790                ConstantProvider::new(schemes[0].clone()),
1791            )
1792            .await;
1793            let marshal2 = setup2.mailbox;
1794
1795            assert!(
1796                marshal2.get_block(&child_digest).await.is_some(),
1797                "certify resolved true for the leader's own proposal so the block must be durable"
1798            );
1799        });
1800    }
1801
1802    /// Shared scenario for the proposal-parent equivocation tests.
1803    ///
1804    /// The local validator certified the view-1 block but only nullified view 2,
1805    /// while the rest of the network notarized and certified the view-2 block.
1806    /// The view-3 leader builds on the view-2 block and broadcasts it, so the
1807    /// block is buffered locally but was never verified here. The equivocating
1808    /// context names the certified view-1 block as parent, which this
1809    /// validator's parent selection accepts (view 1 certified, view 2 nullified).
1810    struct EquivocationFixture {
1811        marshaled: Deferred<deterministic::Context, S, MockVerifyingApp<B, S>, B, FixedEpocher>,
1812        round: Round,
1813        digest: <B as Digestible>::Digest,
1814        embedded_ctx: Ctx,
1815        equivocating_ctx: Ctx,
1816        _extra: <StandardHarness as TestHarness>::ValidatorExtra,
1817    }
1818
1819    async fn equivocation_fixture(
1820        context: &mut deterministic::Context,
1821        app: MockVerifyingApp<B, S>,
1822    ) -> EquivocationFixture {
1823        let Fixture {
1824            participants,
1825            schemes,
1826            ..
1827        } = bls12381_threshold_vrf::fixture::<V, _>(context, NAMESPACE, NUM_VALIDATORS);
1828        let mut oracle = setup_network_with_participants(
1829            context.child("network"),
1830            NZUsize!(1),
1831            participants.clone(),
1832        )
1833        .await;
1834
1835        let me = participants[0].clone();
1836        let setup = StandardHarness::setup_validator(
1837            context.child("validator").with_attribute("index", 0),
1838            &mut oracle,
1839            me,
1840            ConstantProvider::new(schemes[0].clone()),
1841        )
1842        .await;
1843        let marshal = setup.mailbox;
1844        let buffer = setup.extra;
1845
1846        let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1847        let leader = participants[1].clone();
1848
1849        // The view-1 block: the parent this validator last certified.
1850        let certified_round = Round::new(Epoch::zero(), View::new(1));
1851        let certified_ctx = Ctx {
1852            round: certified_round,
1853            leader: default_leader(),
1854            parent: (View::zero(), genesis.digest()),
1855        };
1856        let certified = B::new::<Sha256>(certified_ctx, genesis.digest(), Height::new(1), 100);
1857        let certified_digest = certified.digest();
1858        assert!(marshal.verified(certified_round, certified).await);
1859
1860        // The view-2 block: notarized by the network but only nullified here,
1861        // so this validator never certified it.
1862        let notarized_round = Round::new(Epoch::zero(), View::new(2));
1863        let notarized_ctx = Ctx {
1864            round: notarized_round,
1865            leader: leader.clone(),
1866            parent: (View::new(1), certified_digest),
1867        };
1868        let notarized = B::new::<Sha256>(notarized_ctx, certified_digest, Height::new(2), 200);
1869        let notarized_digest = notarized.digest();
1870        assert!(marshal.verified(notarized_round, notarized).await);
1871
1872        // The view-3 block builds on the view-2 block. The leader's broadcast
1873        // delivered it into the local buffer without a local verification.
1874        let round = Round::new(Epoch::zero(), View::new(3));
1875        let embedded_ctx = Ctx {
1876            round,
1877            leader: leader.clone(),
1878            parent: (View::new(2), notarized_digest),
1879        };
1880        let block = B::new::<Sha256>(embedded_ctx.clone(), notarized_digest, Height::new(3), 300);
1881        let digest = block.digest();
1882        assert!(
1883            buffer
1884                .broadcast(commonware_p2p::Recipients::Some(vec![]), block)
1885                .accepted(),
1886            "buffer broadcast for the candidate should be accepted"
1887        );
1888
1889        let equivocating_ctx = Ctx {
1890            round,
1891            leader,
1892            parent: (View::new(1), certified_digest),
1893        };
1894
1895        let marshaled = Deferred::new(
1896            context.child("deferred"),
1897            app,
1898            marshal,
1899            FixedEpocher::new(BLOCKS_PER_EPOCH),
1900        );
1901        context.sleep(Duration::from_millis(10)).await;
1902
1903        EquivocationFixture {
1904            marshaled,
1905            round,
1906            digest,
1907            embedded_ctx,
1908            equivocating_ctx,
1909            _extra: buffer,
1910        }
1911    }
1912
1913    /// A leader can equivocate at the proposal layer: sign one proposal for a
1914    /// digest declaring the notarized view-2 parent (sent to the validators
1915    /// that certified view 2) and another declaring the certified view-1
1916    /// parent (sent to a validator that only nullified view 2). Both pass
1917    /// their recipients' parent selection and carry the same digest because
1918    /// they name the same block.
1919    ///
1920    /// Refusing to notarize the mismatched proposal is correct. That refusal
1921    /// must not outlive the proposal: once the honest notarization for
1922    /// `(round, digest)` arrives, certification must judge the block against
1923    /// its embedded context (defended by the notarizing quorum) and succeed.
1924    /// Adopting the verdict computed under the equivocating context wedges
1925    /// this validator in the view: the other honest validators have certified
1926    /// and advanced, leaving too few validators to form either a nullification
1927    /// or a finalization after the Byzantine validator stops participating.
1928    #[test_traced("WARN")]
1929    fn test_certify_not_poisoned_by_equivocating_parent_verify() {
1930        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1931        runner.start(|mut context| async move {
1932            let mut fixture = equivocation_fixture(&mut context, MockVerifyingApp::new()).await;
1933
1934            let verify_rx = fixture
1935                .marshaled
1936                .verify(fixture.equivocating_ctx.clone(), fixture.digest)
1937                .await;
1938            assert!(
1939                !verify_rx.await.expect("verify result missing"),
1940                "the equivocating proposal must not be notarized"
1941            );
1942
1943            let certify_rx = fixture
1944                .marshaled
1945                .certify(fixture.round, fixture.digest)
1946                .await;
1947            select! {
1948                result = certify_rx => {
1949                    assert!(
1950                        result.expect("certify result missing"),
1951                        "certify of the notarized digest must not adopt the verdict computed under the equivocating context"
1952                    );
1953                },
1954                _ = context.sleep(Duration::from_secs(5)) => {
1955                    panic!("certify should resolve promptly");
1956                },
1957            }
1958        });
1959    }
1960
1961    /// Control for the equivocation tests: a live application rejection under
1962    /// the matching context is a real verdict. Certification must keep
1963    /// honoring it, because deferred voting means a notarization can exist
1964    /// for an application-invalid block.
1965    #[test_traced("WARN")]
1966    fn test_certify_honors_application_rejection() {
1967        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1968        runner.start(|mut context| async move {
1969            let mut fixture =
1970                equivocation_fixture(&mut context, MockVerifyingApp::with_verify_result(false))
1971                    .await;
1972
1973            let verify_rx = fixture
1974                .marshaled
1975                .verify(fixture.embedded_ctx.clone(), fixture.digest)
1976                .await;
1977            assert!(
1978                verify_rx.await.expect("verify result missing"),
1979                "optimistic verify accepts an available block with a matching context"
1980            );
1981
1982            let certify_rx = fixture
1983                .marshaled
1984                .certify(fixture.round, fixture.digest)
1985                .await;
1986            select! {
1987                result = certify_rx => {
1988                    assert!(
1989                        !result.expect("certify result missing"),
1990                        "certify must propagate the application rejection"
1991                    );
1992                },
1993                _ = context.sleep(Duration::from_secs(5)) => {
1994                    panic!("certify should resolve promptly");
1995                },
1996            }
1997        });
1998    }
1999
2000    /// Without a registered gate (never verified locally, or the gate was lost
2001    /// to a restart), certification runs the application against the block's
2002    /// embedded context. A live rejection there must reach consensus too.
2003    #[test_traced("WARN")]
2004    fn test_certify_without_prior_verify_honors_application_rejection() {
2005        let runner = deterministic::Runner::timed(Duration::from_secs(30));
2006        runner.start(|mut context| async move {
2007            let mut fixture =
2008                equivocation_fixture(&mut context, MockVerifyingApp::with_verify_result(false))
2009                    .await;
2010
2011            // No prior verify, so no gate exists and certify falls through to
2012            // the embedded-context path.
2013            let certify_rx = fixture
2014                .marshaled
2015                .certify(fixture.round, fixture.digest)
2016                .await;
2017            select! {
2018                result = certify_rx => {
2019                    assert!(
2020                        !result.expect("certify result missing"),
2021                        "certify must propagate the application rejection"
2022                    );
2023                },
2024                _ = context.sleep(Duration::from_secs(5)) => {
2025                    panic!("certify should resolve promptly");
2026                },
2027            }
2028        });
2029    }
2030}