Skip to main content

commonware_consensus/marshal/standard/
inline.rs

1//! Wrapper for standard marshal with inline verification.
2//!
3//! # Overview
4//!
5//! [`Inline`] adapts any [`Application`] to the marshal/consensus interfaces
6//! while keeping block validation in the [`Automaton::verify`] path. Unlike
7//! [`super::Deferred`], it does not defer application verification to certification.
8//! Instead, it only reports `true` from `verify` after parent/height checks and
9//! application verification complete.
10//!
11//! # Epoch Boundaries
12//!
13//! As with [`super::Deferred`], when the parent is the last block of the epoch,
14//! [`Inline`] re-proposes that boundary block instead of building a new block.
15//! This prevents proposing blocks that would be excluded by epoch transition.
16//!
17//! # Verification Model
18//!
19//! Inline mode intentionally avoids relying on embedded block context. This allows
20//! usage with block types that implement [`crate::Block`] but not
21//! [`crate::CertifiableBlock`].
22//!
23//! Because verification is completed inline, `certify` must only wait for data
24//! availability in marshal. No additional deferred verification state needs to
25//! be awaited at certify time.
26//!
27//! All validators must run the same wrapper for all views in a given epoch (see
28//! [`crate::marshal::standard`] for more details).
29//!
30//! # Usage
31//!
32//! Wrap your [`Application`] implementation with [`Inline::new`] and provide it to your
33//! consensus engine for the [`Automaton`], [`Relay`], and [`Reporter`]. As the reporter, the
34//! wrapper forwards consensus activity to marshal and releases the state it keeps for each round
35//! once consensus finalizes that round.
36//!
37//! ```rust,ignore
38//! let application = Inline::new(
39//!     context,
40//!     my_application,
41//!     marshal_mailbox,
42//!     epocher,
43//! );
44//! ```
45//!
46//! # When to Use
47//!
48//! Prefer this wrapper when:
49//! - Your application block type is not certifiable.
50//! - You prefer simpler verification semantics over deferred verification latency hiding.
51//! - You are willing to perform full application verification before casting a notarize vote.
52
53use crate::{
54    Application, Automaton, Block, CertifiableAutomaton, Epochable, Handoff, Heightable, Relay,
55    Reporter,
56    marshal::{
57        ancestry::Ancestry,
58        application::{
59            build::{self, Resolved},
60            gates::{GateOutcome, Gates},
61            relay,
62        },
63        core::{CommitmentFallback, DigestFallback, Mailbox},
64        standard::{
65            Standard,
66            validation::{
67                Decision, ParentCheck, await_and_validate_parent, precheck_epoch_and_reproposal,
68                run_app_verify,
69            },
70        },
71    },
72    simplex::{
73        Plan,
74        types::{Activity, Context},
75    },
76    types::{Epocher, Round, View},
77};
78use commonware_actor::Feedback;
79use commonware_codec::Codec;
80use commonware_cryptography::{Digested, Digestible, certificate::Scheme};
81use commonware_macros::select;
82use commonware_runtime::{
83    Clock, Metrics, Spawner,
84    telemetry::{
85        metrics::{
86            MetricsExt as _,
87            histogram::{Buckets, Timed},
88        },
89        traces::TracedExt as _,
90    },
91};
92use commonware_utils::channel::{fallible::OneshotExt, oneshot};
93use std::{
94    future::{Future, ready},
95    sync::Arc,
96};
97use tracing::{Instrument as _, debug, info_span};
98
99/// Waits for a marshal block subscription while allowing consensus to cancel the work.
100async fn await_block_subscription<T, D>(
101    tx: &mut oneshot::Sender<bool>,
102    block_rx: oneshot::Receiver<T>,
103    digest: &D,
104    stage: &'static str,
105) -> Option<T>
106where
107    D: std::fmt::Debug,
108{
109    select! {
110        _ = tx.closed() => {
111            debug!(
112                stage,
113                reason = "consensus dropped receiver",
114                "skipping block wait"
115            );
116            None
117        },
118        result = block_rx => {
119            if result.is_err() {
120                debug!(
121                    stage,
122                    ?digest,
123                    reason = "failed to fetch block",
124                    "skipping block wait"
125                );
126            }
127            result.ok()
128        },
129    }
130}
131
132/// Standard marshal wrapper that verifies blocks inline in `verify`.
133///
134/// # Ancestry Validation
135///
136/// [`Inline`] always validates immediate ancestry before invoking application
137/// verification:
138/// - Parent digest matches consensus context's expected parent
139/// - Child height is exactly parent height plus one
140///
141/// This is sufficient because the parent was either notarized by consensus or verified locally
142/// before this participant voted for it, so its own linkage was already checked.
143///
144/// # Certifiability
145///
146/// This wrapper requires [`crate::Block`] and [`Codec`] for `B`. It does not
147/// require [`crate::CertifiableBlock`]. It is designed for applications that cannot
148/// recover consensus context directly from block payloads.
149pub struct Inline<E, S, A, B, ES>
150where
151    S: Scheme,
152    A: Application<E>,
153    B: Block + Codec,
154    ES: Epocher,
155{
156    context: Arc<E>,
157    application: A,
158    marshal: Mailbox<S, Standard<B>>,
159    epocher: ES,
160    gates: Gates<B::Digest, Digested<B>>,
161
162    build_duration: Timed,
163    proposal_parent_fetch_duration: Timed,
164    ancestor_fetch_duration: Timed,
165}
166
167impl<E, S, A, B, ES> Clone for Inline<E, S, A, B, ES>
168where
169    S: Scheme,
170    A: Application<E>,
171    B: Block + Codec,
172    ES: Epocher,
173{
174    fn clone(&self) -> Self {
175        Self {
176            context: self.context.clone(),
177            application: self.application.clone(),
178            marshal: self.marshal.clone(),
179            epocher: self.epocher.clone(),
180            gates: self.gates.clone(),
181            build_duration: self.build_duration.clone(),
182            proposal_parent_fetch_duration: self.proposal_parent_fetch_duration.clone(),
183            ancestor_fetch_duration: self.ancestor_fetch_duration.clone(),
184        }
185    }
186}
187
188impl<E, S, A, B, ES> Inline<E, S, A, B, ES>
189where
190    E: Metrics + Clock,
191    S: Scheme,
192    A: Application<
193            E,
194            Block = B,
195            SigningScheme = S,
196            Context = Context<B::Digest, S::PublicKey>,
197            Input = (),
198        >,
199    B: Block + Codec,
200    ES: Epocher,
201{
202    /// Creates a new inline-verification wrapper.
203    ///
204    /// Registers a `build_duration` histogram for proposal latency.
205    pub fn new(context: E, application: A, marshal: Mailbox<S, Standard<B>>, epocher: ES) -> Self {
206        let build_histogram = context.histogram(
207            "build_duration",
208            "Histogram of time taken for the application to build a new block, in seconds",
209            Buckets::LOCAL,
210        );
211        let build_duration = Timed::new(build_histogram);
212        let parent_fetch_histogram = context.histogram(
213            "parent_fetch_duration",
214            "Histogram of time taken to fetch a parent block in proposal, in seconds",
215            Buckets::LOCAL,
216        );
217        let proposal_parent_fetch_duration = Timed::new(parent_fetch_histogram);
218        let ancestor_fetch_histogram = context.histogram(
219            "ancestor_fetch_duration",
220            "Histogram of time taken to fetch a block via the ancestry stream, in seconds",
221            Buckets::LOCAL,
222        );
223        let ancestor_fetch_duration = Timed::new(ancestor_fetch_histogram);
224
225        Self {
226            context: Arc::new(context),
227            application,
228            marshal,
229            epocher,
230            gates: Gates::new(),
231            build_duration,
232            proposal_parent_fetch_duration,
233            ancestor_fetch_duration,
234        }
235    }
236
237    /// Runs the proposal checks for `consensus_context` and yields what the proposal path does
238    /// next: re-propose the epoch boundary block, decline a parent the marshal cannot build on,
239    /// or build on the fetched parent.
240    ///
241    /// The returned future borrows nothing from `self`, so it can move into the spawned prepare
242    /// or propose task.
243    fn checks(
244        &self,
245        consensus_context: Context<B::Digest, S::PublicKey>,
246    ) -> impl Future<Output = Resolved<B::Digest, Digested<B>, impl Ancestry<B>>> + Send + 'static
247    {
248        let context = self.context.clone();
249        let marshal = self.marshal.clone();
250        let epocher = self.epocher.clone();
251        let proposal_parent_fetch_duration = self.proposal_parent_fetch_duration.clone();
252        let ancestor_fetch_duration = self.ancestor_fetch_duration.clone();
253        let build_duration = self.build_duration.clone();
254        async move {
255            // The parent for any consensus context is in the same epoch: the
256            // boundary block of the previous epoch is the genesis block of the
257            // current epoch.
258            //
259            // Proposal context carries the parent view and commitment but not
260            // the parent height. The parent may sit above the finalized tip and
261            // may still be uncertified, so this must stay round-bound until the
262            // block is returned.
263            let (parent_view, parent_commitment) = consensus_context.parent;
264            let parent_request = marshal.subscribe_by_commitment(
265                parent_commitment,
266                CommitmentFallback::FetchByRound {
267                    round: Round::new(consensus_context.epoch(), parent_view),
268                },
269            );
270            let parent_timer = proposal_parent_fetch_duration.timer(&*context);
271            let Ok(parent) = parent_request.await else {
272                debug!(
273                    ?parent_commitment,
274                    reason = "failed to fetch parent block",
275                    "skipping proposal"
276                );
277                return Resolved::Skip;
278            };
279            parent_timer.observe(&*context);
280
281            // At epoch boundary, re-propose the parent block.
282            let last_in_epoch = epocher
283                .last(consensus_context.epoch())
284                .expect("current epoch should exist");
285            if parent.height() == last_in_epoch {
286                return Resolved::Reuse(parent.digest(), parent);
287            }
288
289            let ancestor_stream =
290                marshal.ancestor_stream(context.clone(), [parent], ancestor_fetch_duration);
291            Resolved::Build(ancestor_stream, build_duration.timer(&*context), ())
292        }
293    }
294}
295
296impl<E, S, A, B, ES> Automaton for Inline<E, S, A, B, ES>
297where
298    E: Spawner + Metrics + Clock,
299    S: Scheme,
300    A: Application<
301            E,
302            Block = B,
303            SigningScheme = S,
304            Context = Context<B::Digest, S::PublicKey>,
305            Input = (),
306        >,
307    B: Block + Codec,
308    ES: Epocher,
309{
310    type Digest = B::Digest;
311    type Context = Context<Self::Digest, S::PublicKey>;
312
313    /// Proposes a new block or re-proposes an epoch boundary block.
314    ///
315    /// Proposal runs in a spawned task and returns a receiver for the resulting digest. The
316    /// block is staged before the digest is delivered and handed to marshal when consensus
317    /// requests the relay broadcast, which persists it after the send. The resulting sync
318    /// handle is awaited only at certification so it overlaps consensus voting. The digest does
319    /// not imply durability on its own. [`CertifiableAutomaton::certify`] awaits the registered
320    /// certification gate before the finalize vote.
321    #[allow(clippy::async_yields_async)]
322    #[tracing::instrument(name = "marshal.inline.propose", level = "info", skip_all, fields(round = %consensus_context.round))]
323    async fn propose(
324        &mut self,
325        consensus_context: Context<Self::Digest, S::PublicKey>,
326    ) -> oneshot::Receiver<Self::Digest> {
327        let checks = self.checks(consensus_context.clone());
328        let span = info_span!(
329            "marshal.inline.propose.task",
330            round = %consensus_context.round
331        );
332        let round = consensus_context.round;
333        let (parent_view, parent_commitment) = consensus_context.parent;
334        build::propose(
335            self.context.as_ref(),
336            &self.application,
337            self.gates.clone(),
338            consensus_context,
339            checks,
340            span,
341            move || {
342                info_span!(
343                    "marshal.inline.application.propose",
344                    round = %round,
345                    parent_view = parent_view.traced(),
346                    parent = %parent_commitment
347                )
348            },
349            |block, ()| ready((block.digest(), block)),
350        )
351    }
352
353    /// Performs complete verification inline.
354    ///
355    /// This method:
356    /// 1. Waits for the block by digest
357    /// 2. Enforces epoch/re-proposal rules
358    /// 3. Fetches and validates the parent relationship
359    /// 4. Runs application verification over ancestry
360    ///
361    /// The notarize vote is cast as soon as application verification completes. The block's
362    /// durable sync is deferred (it runs concurrently with consensus voting) and its
363    /// completion is registered in `gates` for [`Self::certify`] to await before
364    /// the finalize vote.
365    #[allow(clippy::async_yields_async)]
366    #[tracing::instrument(name = "marshal.inline.verify", level = "info", skip_all, fields(round = %context.round, digest = %digest))]
367    async fn verify(
368        &mut self,
369        context: Context<Self::Digest, S::PublicKey>,
370        digest: Self::Digest,
371    ) -> oneshot::Receiver<bool> {
372        let round = context.round;
373
374        // Verification needs the full block but waits only for local delivery. Certification starts
375        // recovery only when the block is not buffered. If a buffered block is evicted before
376        // verification registers its wait, verification is left with neither the block nor an
377        // active fetch. Register the wait before publishing the gate so it receives the buffered
378        // block or is waiting when recovery delivers it.
379        let block_request = self
380            .marshal
381            .subscribe_by_digest(digest, DigestFallback::Wait);
382        let (durable_tx, durable_rx) = oneshot::channel();
383        self.gates.insert(round, digest, durable_rx);
384
385        let marshal = self.marshal.clone();
386        let mut application = self.application.clone();
387        let epocher = self.epocher.clone();
388        let ancestor_fetch_duration = self.ancestor_fetch_duration.clone();
389
390        let (mut tx, rx) = oneshot::channel();
391        let runtime_context = self
392            .context
393            .child("inline_verify")
394            .with_attribute("round", round);
395        let span = info_span!(
396            "marshal.inline.verify.task",
397            round = %round,
398            digest = %digest
399        );
400        runtime_context.spawn(move |runtime_context| {
401            async move {
402                // Start the parent fetch immediately: its commitment and certified
403                // round are known from the consensus context, so it can proceed in
404                // parallel with broadcast delivery of the candidate block.
405                // Reproposals (digest == context.parent.1) skip parent validation
406                // entirely, so they must not fetch: the "parent" is the candidate
407                // itself, and candidate acquisition is deliberately local-only.
408                let parent_request = (digest != context.parent.1).then(|| {
409                    let (parent_view, parent_commitment) = context.parent;
410                    marshal.subscribe_by_commitment(
411                        parent_commitment,
412                        CommitmentFallback::FetchByRound {
413                            round: Round::new(context.epoch(), parent_view),
414                        },
415                    )
416                });
417
418                let Some(block) =
419                    await_block_subscription(&mut tx, block_request, &digest, "verification").await
420                else {
421                    return;
422                };
423
424                // Shared pre-checks:
425                // - Blocks are invalid if they are not in the expected epoch and are
426                //   not a valid boundary re-proposal.
427                // - Re-proposals are detected when `digest == context.parent.1`.
428                // - Re-proposals skip normal parent/height checks because:
429                //   1) consensus settles their validity when certifying the view that
430                //      first carried the block
431                //   2) parent-child checks would fail by construction when parent == block
432                let Some(decision) =
433                    precheck_epoch_and_reproposal(&epocher, &marshal, &context, digest, block)
434                        .await
435                else {
436                    return;
437                };
438                let block = match decision {
439                    Decision::Complete(valid) => {
440                        // Re-proposal: a valid precheck already persisted the block
441                        // (durable), an invalid one is an epoch rejection. The re-proposal
442                        // reading is an artifact of this header's declared parent: a
443                        // conflicting header naming the block's real parent lets honest
444                        // validators verify the same block as a normal proposal and
445                        // notarize `(round, digest)`, so certification must recover
446                        // rather than adopt the rejection.
447                        tx.send_lossy(valid);
448                        durable_tx.send_lossy(if valid {
449                            GateOutcome::Ready(true)
450                        } else {
451                            GateOutcome::Recover
452                        });
453                        return;
454                    }
455                    Decision::Continue(block) => block,
456                };
457
458                // `Continue` implies a non-reproposal, so the parent subscription
459                // was started above.
460                let parent_request =
461                    parent_request.expect("non-reproposal has a parent subscription");
462
463                // Start the candidate store immediately: it depends on neither the
464                // parent fetch (which may hit the network) nor the verdict below.
465                // Storing before validation is intentional: these caches provide
466                // candidate availability/recovery, not a validity decision. The
467                // notarize vote follows the app verdict, while certify independently
468                // awaits the registered durability gate.
469                //
470                // The verify future below aborts when consensus drops its receiver
471                // (the view exited via nullification or finalization), even though
472                // certification can still fire for a nullified view. That is
473                // deliberate: inline certification does not need the local app
474                // verdict (a notarization implies f+1 honest validators already
475                // verified), and the store still completes through the join.
476                let store = async {
477                    if marshal.verified(round, block.clone()).await {
478                        durable_tx.send_lossy(GateOutcome::Ready(true));
479                    }
480                };
481                let verify_then_vote = async {
482                    // Non-reproposal path: validate the parent we already started
483                    // fetching.
484                    let parent = match await_and_validate_parent(
485                        context.parent.1,
486                        &block,
487                        parent_request,
488                        &mut tx,
489                    )
490                    .await
491                    {
492                        Some(ParentCheck::Valid(parent)) => parent,
493                        Some(ParentCheck::Invalid) => {
494                            tx.send_lossy(false);
495                            return Some(false);
496                        }
497                        None => return None,
498                    };
499                    let valid = run_app_verify(
500                        runtime_context,
501                        context,
502                        block.clone(),
503                        parent,
504                        &mut application,
505                        &marshal,
506                        &mut tx,
507                        ancestor_fetch_duration,
508                    )
509                    .await;
510                    if let Some(valid) = valid {
511                        tx.send_lossy(valid);
512                    }
513                    valid
514                };
515                futures::join!(verify_then_vote, store);
516            }
517            .instrument(span)
518        });
519        rx
520    }
521}
522
523/// Inline certification consumes a registered certification gate when present, and
524/// falls back to a round-bound fetch/persist path when the gate is missing (after
525/// an unclean restart) or cannot speak for the notarized proposal.
526impl<E, S, A, B, ES> CertifiableAutomaton for Inline<E, S, A, B, ES>
527where
528    E: Spawner + Metrics + Clock,
529    S: Scheme,
530    A: Application<
531            E,
532            Block = B,
533            SigningScheme = S,
534            Context = Context<B::Digest, S::PublicKey>,
535            Input = (),
536        >,
537    B: Block + Codec,
538    ES: Epocher,
539{
540    /// Prepares a term-start proposal on an uncertified parent.
541    ///
542    /// Runs the same checks as [`Self::propose`], in the same order, and hands the application the
543    /// fetched ancestry. When the checks find the epoch boundary block, the marshal re-proposes it
544    /// without the application and answers [`Handoff::Stage`].
545    #[allow(clippy::async_yields_async)]
546    #[tracing::instrument(name = "marshal.inline.prepare", level = "info", skip_all, fields(round = %consensus_context.round))]
547    async fn prepare(
548        &mut self,
549        consensus_context: Context<Self::Digest, S::PublicKey>,
550    ) -> oneshot::Receiver<Handoff<Self::Digest>> {
551        let checks = self.checks(consensus_context.clone());
552        let span = info_span!(
553            "marshal.inline.prepare.task",
554            round = %consensus_context.round
555        );
556        let round = consensus_context.round;
557        let (parent_view, parent_commitment) = consensus_context.parent;
558        build::prepare(
559            self.context.as_ref(),
560            &self.application,
561            self.gates.clone(),
562            consensus_context,
563            checks,
564            span,
565            move || {
566                info_span!(
567                    "marshal.inline.application.prepare",
568                    round = %round,
569                    parent_view = parent_view.traced(),
570                    parent = %parent_commitment
571                )
572            },
573            |block, ()| ready((block.digest(), block)),
574        )
575    }
576
577    #[allow(clippy::async_yields_async)]
578    #[tracing::instrument(name = "marshal.inline.certify", level = "info", skip_all, fields(round = %round, digest = %digest))]
579    async fn certify(&mut self, round: Round, digest: Self::Digest) -> oneshot::Receiver<bool> {
580        // `prepare`, `propose`, and `verify` register an in-flight certification gate whose
581        // result resolves once the block's sync handle completes. Awaiting it here is the
582        // durability barrier for the finalize vote, and it lets the sync overlap consensus voting
583        // instead of freezing certify with a fresh fsync. A proposal still staged here is this
584        // node's own proposal, so `verified_deferred` stores it and defers only its durable-sync
585        // handle, which it delivers through the staged ack.
586        let task = self.gates.claim(round, digest, |block, ack| {
587            self.marshal.verified_deferred(round, block, ack)
588        });
589
590        // `verify()` waits only on local broadcast delivery, so nudge a
591        // round-bound notarized fetch that can unblock the existing waiter
592        // if local broadcast never arrives. For the standard variant, the
593        // digest is also the variant commitment.
594        if task.is_some() {
595            self.marshal.hint_notarized(round, digest);
596        }
597        let marshal = self.marshal.clone();
598        let (mut tx, rx) = oneshot::channel();
599        let context = self
600            .context
601            .child("inline_certify")
602            .with_attribute("round", round);
603        context.spawn(move |_| {
604            async move {
605                // A ready gate proves the notarized block is durable. A gate that
606                // cannot speak for the notarized proposal falls through to recovery.
607                if let Some(task) = task {
608                    let result = select! {
609                        _ = tx.closed() => {
610                            debug!(
611                                reason = "consensus dropped receiver",
612                                "skipping certification"
613                            );
614                            return;
615                        },
616                        result = task => result,
617                    };
618                    match result {
619                        Ok(GateOutcome::Ready(verdict)) => {
620                            tx.send_lossy(verdict);
621                            return;
622                        }
623                        Ok(GateOutcome::Recover) | Err(_) => {}
624                    }
625                }
626
627                // No local certification gate task (for example after an unclean restart):
628                // fetch the notarized block and persist it. A Byzantine leader can form a
629                // notarization after sending the proposal to only f+1 honest validators, so
630                // the validators left without the block must fetch it here to certify and
631                // avoid getting stuck.
632                let block_rx =
633                    marshal.subscribe_by_digest(digest, DigestFallback::FetchByRound { round });
634                let Some(block) =
635                    await_block_subscription(&mut tx, block_rx, &digest, "certification").await
636                else {
637                    return;
638                };
639                if !marshal.certified(round, block).await {
640                    return;
641                }
642                tx.send_lossy(true);
643            }
644            .instrument(info_span!(
645                "marshal.inline.certify.task",
646                round = %round,
647                digest = %digest
648            ))
649        });
650
651        rx
652    }
653}
654
655impl<E, S, A, B, ES> Relay for Inline<E, S, A, B, ES>
656where
657    E: Send + Sync + 'static,
658    S: Scheme,
659    A: Application<E, Block = B, Context = Context<B::Digest, S::PublicKey>>,
660    B: Block + Codec,
661    ES: Epocher,
662{
663    type Digest = B::Digest;
664    type PublicKey = S::PublicKey;
665    type Plan = Plan<S::PublicKey>;
666
667    fn broadcast(&mut self, commitment: Self::Digest, plan: Plan<S::PublicKey>) -> Feedback {
668        relay::broadcast(&self.gates, &self.marshal, commitment, plan)
669    }
670}
671
672/// The wrapper is the consensus engine's reporter, as it is its automaton and relay: it forwards
673/// every activity to marshal and first releases the state it keeps per round (certification
674/// gates, staged proposals, and sent marks). A finalization releases its round and the rounds
675/// below it. Any activity of a later epoch releases every earlier epoch, whose engine has been
676/// retired, which covers finalized progress that marshal imported while no engine reported it.
677/// The voter reports from the same task that requests the lock-in broadcast and certification,
678/// so pruning is ordered with both.
679impl<E, S, A, B, ES> Reporter for Inline<E, S, A, B, ES>
680where
681    E: Send + Sync + 'static,
682    S: Scheme,
683    A: Application<E, Block = B, Context = Context<B::Digest, S::PublicKey>>,
684    B: Block + Codec,
685    ES: Epocher,
686{
687    type Activity = Activity<S, B::Digest>;
688
689    fn report(&mut self, activity: Self::Activity) -> Feedback {
690        let retired = match &activity {
691            Activity::Finalization(finalization) => finalization.round(),
692            activity => Round::new(activity.epoch(), View::zero()),
693        };
694        self.gates.retain_after(retired);
695        self.marshal.report(activity)
696    }
697}
698
699#[cfg(test)]
700mod tests {
701    use super::Inline;
702    use crate::{
703        Application, Automaton, Block, CertifiableAutomaton, Relay,
704        marshal::{
705            application::gates::Gates,
706            mocks::{
707                harness::{
708                    B, BLOCKS_PER_EPOCH, Ctx, D, NAMESPACE, NUM_VALIDATORS, S, StandardHarness,
709                    TestHarness, V, default_leader, make_raw_block,
710                    setup_network_with_participants,
711                },
712                verifying::{GatedVerifyingApp, MockVerifyingApp},
713            },
714            standard::tests::ready,
715        },
716        simplex::{scheme::bls12381_threshold::vrf as bls12381_threshold_vrf, types::Context},
717        types::{Epoch, FixedEpocher, Height, Round, View},
718    };
719    use commonware_broadcast::Broadcaster;
720    use commonware_codec::Codec;
721    use commonware_cryptography::{
722        Digested, Digestible, Hasher as _,
723        certificate::{ConstantProvider, Scheme, mocks::Fixture},
724        sha256::Sha256,
725    };
726    use commonware_macros::{select, test_traced};
727    use commonware_runtime::{Clock, Metrics, Runner, Spawner, Supervisor as _, deterministic};
728    use commonware_utils::{NZUsize, channel::fallible::OneshotExt};
729    use std::time::Duration;
730
731    // Compile-time assertion only: inline standard wrapper must not require `CertifiableBlock`.
732    #[allow(dead_code)]
733    fn assert_non_certifiable_block_supported<E, S, A, B, ES>()
734    where
735        E: Spawner + Metrics + Clock,
736        S: Scheme,
737        A: Application<
738                E,
739                Block = B,
740                SigningScheme = S,
741                Context = Context<B::Digest, S::PublicKey>,
742                Input = (),
743            >,
744        B: Block + Codec,
745        ES: crate::types::Epocher,
746    {
747        fn assert_automaton<T: Automaton>() {}
748        fn assert_certifiable<T: CertifiableAutomaton>() {}
749        fn assert_relay<T: Relay>() {}
750
751        assert_automaton::<Inline<E, S, A, B, ES>>();
752        assert_certifiable::<Inline<E, S, A, B, ES>>();
753        assert_relay::<Inline<E, S, A, B, ES>>();
754    }
755
756    #[test_traced("INFO")]
757    fn test_certify_returns_immediately_after_verify_fetches_block() {
758        let runner = deterministic::Runner::timed(Duration::from_secs(30));
759        runner.start(|mut context| async move {
760            let Fixture {
761                participants,
762                schemes,
763                ..
764            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
765            let mut oracle = setup_network_with_participants(
766                context.child("network"),
767                NZUsize!(1),
768                participants.clone(),
769                1024 * 1024,
770            )
771            .await;
772
773            let me = participants[0].clone();
774            let setup = StandardHarness::setup_validator(
775                context.child("validator").with_attribute("index", 0),
776                &mut oracle,
777                me.clone(),
778                ConstantProvider::new(schemes[0].clone()),
779            )
780            .await;
781            let marshal = setup.mailbox;
782
783            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
784            let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
785            let mut inline = Inline::new(
786                context.child("inline"),
787                mock_app,
788                marshal.clone(),
789                FixedEpocher::new(BLOCKS_PER_EPOCH),
790            );
791
792            // Seed the parent and child blocks in marshal so verify can fetch locally.
793            let parent_round = Round::new(Epoch::zero(), View::new(1));
794            let parent_ctx = Ctx {
795                round: parent_round,
796                leader: default_leader(),
797                parent: (View::zero(), genesis.digest()),
798            };
799            let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
800            let parent_digest = parent.digest();
801            assert!(marshal.verified(parent_round, ready(parent)).await);
802
803            let round = Round::new(Epoch::zero(), View::new(2));
804            let verify_context = Ctx {
805                round,
806                leader: me,
807                parent: (View::new(1), parent_digest),
808            };
809            let block =
810                B::new::<Sha256>(verify_context.clone(), parent_digest, Height::new(2), 200);
811            let digest = block.digest();
812            assert!(marshal.verified(round, ready(block)).await);
813
814            // Complete verify first so the block is already available locally.
815            let verify_rx = inline.verify(verify_context, digest).await;
816            assert!(
817                verify_rx.await.unwrap(),
818                "verify should complete successfully before certify"
819            );
820
821            // Certify should return immediately instead of waiting on marshal.
822            let certify_rx = inline.certify(round, digest).await;
823
824            select! {
825                result = certify_rx => {
826                    assert!(
827                        result.unwrap(),
828                        "certify should return immediately once verify has fetched the block"
829                    );
830                },
831                _ = context.sleep(Duration::from_secs(5)) => {
832                    panic!("certify should not hang after local verify completed");
833                },
834            }
835        });
836    }
837
838    #[test_traced("INFO")]
839    fn test_certify_succeeds_without_verify_task() {
840        let runner = deterministic::Runner::timed(Duration::from_secs(30));
841        runner.start(|mut context| async move {
842            let Fixture {
843                participants,
844                schemes,
845                ..
846            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
847            let mut oracle = setup_network_with_participants(
848                context.child("network"),
849                NZUsize!(1),
850                participants.clone(),
851                1024 * 1024,
852            )
853            .await;
854
855            let me = participants[0].clone();
856            let setup = StandardHarness::setup_validator(
857                context.child("validator").with_attribute("index", 0),
858                &mut oracle,
859                me.clone(),
860                ConstantProvider::new(schemes[0].clone()),
861            )
862            .await;
863            let marshal = setup.mailbox;
864
865            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
866            let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
867            let mut inline = Inline::new(
868                context.child("inline"),
869                mock_app,
870                marshal.clone(),
871                FixedEpocher::new(BLOCKS_PER_EPOCH),
872            );
873
874            // Seed the parent and child blocks in marshal without starting a verify task.
875            let parent_round = Round::new(Epoch::zero(), View::new(1));
876            let parent_ctx = Ctx {
877                round: parent_round,
878                leader: default_leader(),
879                parent: (View::zero(), genesis.digest()),
880            };
881            let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
882            let parent_digest = parent.digest();
883            assert!(marshal.verified(parent_round, ready(parent)).await);
884
885            let round = Round::new(Epoch::zero(), View::new(2));
886            let verify_context = Ctx {
887                round,
888                leader: me,
889                parent: (View::new(1), parent_digest),
890            };
891            let block =
892                B::new::<Sha256>(verify_context.clone(), parent_digest, Height::new(2), 200);
893            let digest = block.digest();
894            assert!(marshal.verified(round, ready(block)).await);
895
896            // Certify should still resolve by waiting on marshal block availability directly.
897            let certify_rx = inline.certify(round, digest).await;
898
899            select! {
900                result = certify_rx => {
901                    assert!(
902                        result.unwrap(),
903                        "certify should resolve once block availability is known"
904                    );
905                },
906                _ = context.sleep(Duration::from_secs(5)) => {
907                    panic!("certify should not hang when block is already available in marshal");
908                },
909            }
910        });
911    }
912
913    #[test_traced("INFO")]
914    fn test_certify_reproposal_uses_available_blocks_after_verify() {
915        let runner = deterministic::Runner::timed(Duration::from_secs(30));
916        runner.start(|mut context| async move {
917            let Fixture {
918                participants,
919                schemes,
920                ..
921            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
922            let mut oracle = setup_network_with_participants(
923                context.child("network"),
924                NZUsize!(1),
925                participants.clone(),
926                1024 * 1024,
927            )
928            .await;
929
930            let me = participants[0].clone();
931            let setup = StandardHarness::setup_validator(
932                context.child("validator").with_attribute("index", 0),
933                &mut oracle,
934                me.clone(),
935                ConstantProvider::new(schemes[0].clone()),
936            )
937            .await;
938            let marshal = setup.mailbox;
939            let marshal_actor_handle = setup.actor_handle;
940
941            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
942            let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
943            let mut inline = Inline::new(context.child("inline"),
944                mock_app,
945                marshal.clone(),
946                FixedEpocher::new(BLOCKS_PER_EPOCH),
947            );
948
949            let boundary_height = Height::new(BLOCKS_PER_EPOCH.get() - 1);
950            let boundary_round = Round::new(Epoch::zero(), View::new(boundary_height.get()));
951            let boundary_block = B::new::<Sha256>(
952                Ctx {
953                    round: boundary_round,
954                    leader: default_leader(),
955                    parent: (View::zero(), genesis.digest()),
956                },
957                genesis.digest(),
958                boundary_height,
959                1900,
960            );
961            let boundary_digest = boundary_block.digest();
962            assert!(
963                marshal.verified(boundary_round, ready(boundary_block)).await
964            );
965
966            let reproposal_round = Round::new(Epoch::zero(), View::new(boundary_height.get() + 1));
967            let reproposal_context = Ctx {
968                round: reproposal_round,
969                leader: me,
970                parent: (View::new(boundary_height.get()), boundary_digest),
971            };
972
973            let verify_rx = inline.verify(reproposal_context, boundary_digest).await;
974            assert!(
975                verify_rx.await.unwrap(),
976                "verify should accept a valid boundary re-proposal"
977            );
978
979            marshal_actor_handle.abort();
980            drop(marshal);
981            context.sleep(Duration::from_millis(1)).await;
982
983            let certify_rx = inline.certify(reproposal_round, boundary_digest).await;
984            select! {
985                result = certify_rx => {
986                    assert!(
987                        result.unwrap(),
988                        "certify should use the available_blocks fast path for verified re-proposals"
989                    );
990                },
991                _ = context.sleep(Duration::from_secs(5)) => {
992                    panic!("certify should not depend on marshal after verify cached a re-proposal");
993                },
994            }
995        });
996    }
997
998    /// A header can only read as a re-proposal under its declared parent, so an
999    /// invalid-precheck rejection (non-boundary block) is scoped to that header,
1000    /// not to the notarized `(round, digest)`. Certification must route through
1001    /// recovery instead of adopting the rejection as its verdict.
1002    #[test_traced("WARN")]
1003    fn test_certify_recovers_from_invalid_reproposal_precheck() {
1004        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1005        runner.start(|mut context| async move {
1006            let Fixture {
1007                participants,
1008                schemes,
1009                ..
1010            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1011            let mut oracle = setup_network_with_participants(
1012                context.child("network"),
1013                NZUsize!(1),
1014                participants.clone(),
1015                1024 * 1024,
1016            )
1017            .await;
1018
1019            let me = participants[0].clone();
1020            let setup = StandardHarness::setup_validator(
1021                context.child("validator").with_attribute("index", 0),
1022                &mut oracle,
1023                me.clone(),
1024                ConstantProvider::new(schemes[0].clone()),
1025            )
1026            .await;
1027            let marshal = setup.mailbox;
1028
1029            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1030            let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1031            let mut inline = Inline::new(
1032                context.child("inline"),
1033                mock_app,
1034                marshal.clone(),
1035                FixedEpocher::new(BLOCKS_PER_EPOCH),
1036            );
1037
1038            // A non-boundary block notarized at view 2 and available locally.
1039            let block_round = Round::new(Epoch::zero(), View::new(2));
1040            let block = B::new::<Sha256>(
1041                Ctx {
1042                    round: block_round,
1043                    leader: default_leader(),
1044                    parent: (View::zero(), genesis.digest()),
1045                },
1046                genesis.digest(),
1047                Height::new(2),
1048                200,
1049            );
1050            let digest = block.digest();
1051            assert!(marshal.verified(block_round, ready(block)).await);
1052
1053            // The view-3 header names the block as its own parent, so the
1054            // precheck reads it as a re-proposal and rejects it (not at the
1055            // epoch boundary).
1056            let round = Round::new(Epoch::zero(), View::new(3));
1057            let reproposal_context = Ctx {
1058                round,
1059                leader: me,
1060                parent: (View::new(2), digest),
1061            };
1062            let verify_rx = inline.verify(reproposal_context, digest).await;
1063            assert!(
1064                !verify_rx.await.expect("verify result missing"),
1065                "a non-boundary re-proposal must be rejected"
1066            );
1067
1068            // The header-scoped rejection must not become the certification
1069            // verdict for the notarized digest.
1070            let certify_rx = inline.certify(round, digest).await;
1071            select! {
1072                result = certify_rx => {
1073                    assert!(
1074                        result.expect("certify result missing"),
1075                        "certify must recover instead of adopting the precheck rejection"
1076                    );
1077                },
1078                _ = context.sleep(Duration::from_secs(5)) => {
1079                    panic!("certify should resolve promptly");
1080                },
1081            }
1082        });
1083    }
1084
1085    /// Regression: `certify` resolving true drives the finalize vote in inline
1086    /// mode, so it must imply the block is durably persisted even when the
1087    /// certify path subscribed before `verify()` finished.
1088    #[test_traced("WARN")]
1089    fn test_inline_certify_persists_block_before_resolving() {
1090        for seed in 0u64..16 {
1091            inline_certify_persists_block_before_resolving_at(seed);
1092        }
1093    }
1094
1095    fn inline_certify_persists_block_before_resolving_at(seed: u64) {
1096        let runner = deterministic::Runner::new(
1097            deterministic::Config::new()
1098                .with_seed(seed)
1099                .with_timeout(Some(Duration::from_secs(60))),
1100        );
1101        runner.start(|mut context| async move {
1102            let Fixture {
1103                participants,
1104                schemes,
1105                ..
1106            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1107            let mut oracle = setup_network_with_participants(
1108                context.child("network"),
1109                NZUsize!(1),
1110                participants.clone(),
1111                1024 * 1024,
1112            )
1113            .await;
1114
1115            let me = participants[0].clone();
1116
1117            let setup = StandardHarness::setup_validator(
1118                context.child("validator").with_attribute("index", 0),
1119                &mut oracle,
1120                me.clone(),
1121                ConstantProvider::new(schemes[0].clone()),
1122            )
1123            .await;
1124            let marshal = setup.mailbox;
1125            let buffer = setup.extra;
1126            let actor_handle = setup.actor_handle;
1127
1128            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1129            let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1130            let mut inline = Inline::new(
1131                context.child("inline"),
1132                mock_app,
1133                marshal.clone(),
1134                FixedEpocher::new(BLOCKS_PER_EPOCH),
1135            );
1136
1137            let parent = make_raw_block(genesis.digest(), Height::new(1), 100);
1138            let parent_digest = parent.digest();
1139
1140            let child_round = Round::new(Epoch::zero(), View::new(2));
1141            let child_ctx = Ctx {
1142                round: child_round,
1143                leader: me.clone(),
1144                parent: (View::new(1), parent_digest),
1145            };
1146            let child = B::new::<Sha256>(child_ctx.clone(), parent_digest, Height::new(2), 200);
1147            let child_digest = child.digest();
1148
1149            assert!(
1150                buffer
1151                    .broadcast(
1152                        commonware_p2p::Recipients::Some(vec![]),
1153                        ready(parent.clone())
1154                    )
1155                    .accepted(),
1156                "buffer broadcast for parent should be accepted"
1157            );
1158            assert!(
1159                buffer
1160                    .broadcast(
1161                        commonware_p2p::Recipients::Some(vec![]),
1162                        ready(child.clone())
1163                    )
1164                    .accepted(),
1165                "buffer broadcast for child should be accepted"
1166            );
1167
1168            let verify_rx = inline.verify(child_ctx, child_digest).await;
1169            let certify_result = inline
1170                .certify(child_round, child_digest)
1171                .await
1172                .await
1173                .expect("certify result missing");
1174            assert!(certify_result, "certify should succeed");
1175
1176            actor_handle.abort();
1177            let _ = actor_handle.await;
1178            drop(verify_rx);
1179            drop(inline);
1180            drop(marshal);
1181            drop(buffer);
1182
1183            let setup2 = StandardHarness::setup_validator(
1184                context
1185                    .child("validator_restart")
1186                    .with_attribute("index", 0),
1187                &mut oracle,
1188                me.clone(),
1189                ConstantProvider::new(schemes[0].clone()),
1190            )
1191            .await;
1192            let marshal2 = setup2.mailbox;
1193
1194            let post_restart = marshal2.get_block(&child_digest).await;
1195            assert!(
1196                post_restart.is_some(),
1197                "certify resolved true so block must be durably persisted (seed={seed})"
1198            );
1199        });
1200    }
1201
1202    /// Regression: in inline mode `propose` registers a certification gate for the
1203    /// built block that `certify` awaits. After the leader certifies its own proposal,
1204    /// the block must be durably recoverable. This is the >=f+1 guarantee: the leader
1205    /// certifies its own block through marshal so it awaits durability before the
1206    /// finalize vote.
1207    #[test_traced("WARN")]
1208    fn test_inline_propose_then_certify_persists_block() {
1209        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1210        runner.start(|mut context| async move {
1211            let Fixture {
1212                participants,
1213                schemes,
1214                ..
1215            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1216            let mut oracle = setup_network_with_participants(
1217                context.child("network"),
1218                NZUsize!(1),
1219                participants.clone(),
1220                1024 * 1024,
1221            )
1222            .await;
1223
1224            let me = participants[0].clone();
1225            let setup = StandardHarness::setup_validator(
1226                context.child("validator").with_attribute("index", 0),
1227                &mut oracle,
1228                me.clone(),
1229                ConstantProvider::new(schemes[0].clone()),
1230            )
1231            .await;
1232            let marshal = setup.mailbox;
1233            let actor_handle = setup.actor_handle;
1234
1235            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1236
1237            // Seed the parent at its round so `propose` can fetch it locally.
1238            let parent_round = Round::new(Epoch::zero(), View::new(1));
1239            let parent_ctx = Ctx {
1240                round: parent_round,
1241                leader: default_leader(),
1242                parent: (View::zero(), genesis.digest()),
1243            };
1244            let parent = B::new::<Sha256>(parent_ctx, genesis.digest(), Height::new(1), 100);
1245            let parent_digest = parent.digest();
1246            assert!(marshal.verified(parent_round, ready(parent)).await);
1247
1248            // The leader builds the child via `app.propose`.
1249            let round = Round::new(Epoch::zero(), View::new(2));
1250            let ctx = Ctx {
1251                round,
1252                leader: me.clone(),
1253                parent: (View::new(1), parent_digest),
1254            };
1255            let child = B::new::<Sha256>(ctx.clone(), parent_digest, Height::new(2), 200);
1256            let child_digest = child.digest();
1257            let mock_app: MockVerifyingApp<B, S> =
1258                MockVerifyingApp::new().with_propose_result(child);
1259            let mut inline = Inline::new(
1260                context.child("inline"),
1261                mock_app,
1262                marshal.clone(),
1263                FixedEpocher::new(BLOCKS_PER_EPOCH),
1264            );
1265
1266            let digest = inline
1267                .propose(ctx)
1268                .await
1269                .await
1270                .expect("propose must return a digest");
1271            assert_eq!(
1272                digest, child_digest,
1273                "propose must return the built block's digest"
1274            );
1275
1276            // The leader certifies its own proposal, which awaits the deferred sync handle.
1277            assert!(
1278                inline
1279                    .certify(round, child_digest)
1280                    .await
1281                    .await
1282                    .expect("certify result missing"),
1283                "certify must succeed for the leader's own proposal"
1284            );
1285
1286            // After certify, the block must be durable across an unclean restart.
1287            actor_handle.abort();
1288            let _ = actor_handle.await;
1289            drop(inline);
1290            drop(marshal);
1291
1292            let setup2 = StandardHarness::setup_validator(
1293                context
1294                    .child("validator_restart")
1295                    .with_attribute("index", 0),
1296                &mut oracle,
1297                me,
1298                ConstantProvider::new(schemes[0].clone()),
1299            )
1300            .await;
1301            let marshal2 = setup2.mailbox;
1302
1303            assert!(
1304                marshal2.get_block(&child_digest).await.is_some(),
1305                "certify resolved true for the leader's own proposal so the block must be durable"
1306            );
1307        });
1308    }
1309
1310    /// Dropping the verify receiver before the block is available closes the
1311    /// synchronously-registered certification gate. `certify` must recover through
1312    /// the fetch/certified path instead of returning the closed gate to consensus.
1313    #[test_traced("WARN")]
1314    fn test_inline_certify_recovers_after_verify_receiver_drop() {
1315        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1316        runner.start(|mut context| async move {
1317            let Fixture {
1318                participants,
1319                schemes,
1320                ..
1321            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1322            let mut oracle = setup_network_with_participants(
1323                context.child("network"),
1324                NZUsize!(1),
1325                participants.clone(),
1326                1024 * 1024,
1327            )
1328            .await;
1329
1330            let me = participants[0].clone();
1331            let setup = StandardHarness::setup_validator(
1332                context.child("validator").with_attribute("index", 0),
1333                &mut oracle,
1334                me.clone(),
1335                ConstantProvider::new(schemes[0].clone()),
1336            )
1337            .await;
1338            let marshal = setup.mailbox;
1339
1340            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1341            let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1342            let mut inline = Inline::new(
1343                context.child("inline"),
1344                mock_app,
1345                marshal.clone(),
1346                FixedEpocher::new(BLOCKS_PER_EPOCH),
1347            );
1348
1349            let round = Round::new(Epoch::zero(), View::new(1));
1350            let block_context = Ctx {
1351                round,
1352                leader: me,
1353                parent: (View::zero(), genesis.digest()),
1354            };
1355            let block =
1356                B::new::<Sha256>(block_context.clone(), genesis.digest(), Height::new(1), 100);
1357            let digest = block.digest();
1358
1359            let verify_rx = inline.verify(block_context, digest).await;
1360            drop(verify_rx);
1361
1362            // Give the verify task a chance to observe the dropped receiver while its
1363            // block subscription is still pending.
1364            context.sleep(Duration::from_millis(10)).await;
1365
1366            assert!(marshal.verified(round, ready(block)).await);
1367            let certify_rx = inline.certify(round, digest).await;
1368            select! {
1369                result = certify_rx => {
1370                    assert!(
1371                        result.expect("certify result missing"),
1372                        "certify should recover after verify receiver drop"
1373                    );
1374                },
1375                _ = context.sleep(Duration::from_secs(5)) => {
1376                    panic!("certify should recover promptly after verify drop");
1377                },
1378            }
1379        });
1380    }
1381
1382    /// The store request runs concurrently with `app.verify`, not after the
1383    /// notarize vote: while gated application verification is still blocked, the
1384    /// block has already reached marshal and is locally queryable even though the
1385    /// sync handle may still be pending. Releasing verification then lets the
1386    /// notarize vote resolve and certification await the registered certification
1387    /// gate. Separate restart tests cover durable recovery after certification.
1388    #[test_traced("WARN")]
1389    fn test_inline_store_overlaps_app_verify() {
1390        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1391        runner.start(|mut context| async move {
1392            let Fixture {
1393                participants,
1394                schemes,
1395                ..
1396            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1397            let mut oracle = setup_network_with_participants(
1398                context.child("network"),
1399                NZUsize!(1),
1400                participants.clone(),
1401                1024 * 1024,
1402            )
1403            .await;
1404
1405            let me = participants[0].clone();
1406
1407            let setup = StandardHarness::setup_validator(
1408                context.child("validator").with_attribute("index", 0),
1409                &mut oracle,
1410                me.clone(),
1411                ConstantProvider::new(schemes[0].clone()),
1412            )
1413            .await;
1414            let marshal = setup.mailbox;
1415            let buffer = setup.extra;
1416
1417            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1418            let (mock_app, verify_started, release_verify): (GatedVerifyingApp<B, S>, _, _) =
1419                GatedVerifyingApp::new();
1420            let mut inline = Inline::new(
1421                context.child("inline"),
1422                mock_app,
1423                marshal.clone(),
1424                FixedEpocher::new(BLOCKS_PER_EPOCH),
1425            );
1426
1427            let parent = make_raw_block(genesis.digest(), Height::new(1), 100);
1428            let parent_digest = parent.digest();
1429
1430            let child_round = Round::new(Epoch::zero(), View::new(2));
1431            let child_ctx = Ctx {
1432                round: child_round,
1433                leader: me.clone(),
1434                parent: (View::new(1), parent_digest),
1435            };
1436            let child = B::new::<Sha256>(child_ctx.clone(), parent_digest, Height::new(2), 200);
1437            let child_digest = child.digest();
1438
1439            assert!(
1440                buffer
1441                    .broadcast(commonware_p2p::Recipients::Some(vec![]), ready(parent))
1442                    .accepted(),
1443                "buffer broadcast for parent should be accepted"
1444            );
1445            assert!(
1446                buffer
1447                    .broadcast(commonware_p2p::Recipients::Some(vec![]), ready(child))
1448                    .accepted(),
1449                "buffer broadcast for child should be accepted"
1450            );
1451
1452            let verify_rx = inline.verify(child_ctx, child_digest).await;
1453
1454            // Application verification is now blocked. The store request runs concurrently
1455            // with it, so the block is locally queryable even though the notarize vote has
1456            // not been cast and the sync handle may still be pending.
1457            verify_started
1458                .await
1459                .expect("verify should reach the gated application");
1460            assert!(
1461                marshal.get_block(&child_digest).await.is_some(),
1462                "the store request runs concurrently with app.verify, so the block is locally queryable while verification is still gated"
1463            );
1464
1465            // Releasing verification resolves the notarize vote and lets certification
1466            // succeed (valid and durable).
1467            release_verify.send_lossy(());
1468            assert!(
1469                verify_rx.await.expect("verify result missing"),
1470                "inline verify should pass once verification is released"
1471            );
1472            let certify_rx = inline.certify(child_round, child_digest).await;
1473            select! {
1474                result = certify_rx => {
1475                    assert!(
1476                        result.expect("certify result missing"),
1477                        "certify should succeed once verification passes"
1478                    );
1479                },
1480                _ = context.sleep(Duration::from_secs(5)) => {
1481                    panic!("certify should resolve after verification is released");
1482                },
1483            }
1484        });
1485    }
1486
1487    /// Inline analog of the deferred equivocation test. A proposal that names
1488    /// the certified view-1 parent for a block built on the view-2 block
1489    /// fails structural parent validation and is rejected for voting. The
1490    /// candidate is nevertheless stored durably, and certification of the
1491    /// honest notarization for the same `(round, digest)` must use that
1492    /// durability result rather than the context-scoped verification verdict.
1493    #[test_traced("WARN")]
1494    fn test_certify_not_poisoned_by_equivocating_parent_verify() {
1495        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1496        runner.start(|mut context| async move {
1497            let Fixture {
1498                participants,
1499                schemes,
1500                ..
1501            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1502            let mut oracle = setup_network_with_participants(
1503                context.child("network"),
1504                NZUsize!(1),
1505                participants.clone(),
1506                1024 * 1024,
1507            )
1508            .await;
1509
1510            let me = participants[0].clone();
1511            let setup = StandardHarness::setup_validator(
1512                context.child("validator").with_attribute("index", 0),
1513                &mut oracle,
1514                me,
1515                ConstantProvider::new(schemes[0].clone()),
1516            )
1517            .await;
1518            let marshal = setup.mailbox;
1519            let buffer = setup.extra;
1520
1521            let genesis = make_raw_block(Sha256::hash(&[b""]), Height::zero(), 0);
1522            let mock_app: MockVerifyingApp<B, S> = MockVerifyingApp::new();
1523            let mut inline = Inline::new(
1524                context.child("inline"),
1525                mock_app,
1526                marshal.clone(),
1527                FixedEpocher::new(BLOCKS_PER_EPOCH),
1528            );
1529
1530            let leader = participants[1].clone();
1531
1532            // The view-1 block: the parent this validator last certified.
1533            let certified_round = Round::new(Epoch::zero(), View::new(1));
1534            let certified_ctx = Ctx {
1535                round: certified_round,
1536                leader: default_leader(),
1537                parent: (View::zero(), genesis.digest()),
1538            };
1539            let certified = B::new::<Sha256>(certified_ctx, genesis.digest(), Height::new(1), 100);
1540            let certified_digest = certified.digest();
1541            assert!(marshal.verified(certified_round, ready(certified)).await);
1542
1543            // The view-2 block: notarized by the network but only nullified
1544            // here, so this validator never certified it.
1545            let notarized_round = Round::new(Epoch::zero(), View::new(2));
1546            let notarized_ctx = Ctx {
1547                round: notarized_round,
1548                leader: leader.clone(),
1549                parent: (View::new(1), certified_digest),
1550            };
1551            let notarized = B::new::<Sha256>(notarized_ctx, certified_digest, Height::new(2), 200);
1552            let notarized_digest = notarized.digest();
1553            assert!(marshal.verified(notarized_round, ready(notarized)).await);
1554
1555            // The view-3 block builds on the view-2 block. The leader's
1556            // broadcast delivered it into the local buffer without a local
1557            // verification.
1558            let round = Round::new(Epoch::zero(), View::new(3));
1559            let block_ctx = Ctx {
1560                round,
1561                leader: leader.clone(),
1562                parent: (View::new(2), notarized_digest),
1563            };
1564            let block = B::new::<Sha256>(block_ctx, notarized_digest, Height::new(3), 300);
1565            let digest = block.digest();
1566            assert!(
1567                buffer
1568                    .broadcast(commonware_p2p::Recipients::Some(vec![]), ready(block))
1569                    .accepted(),
1570                "buffer broadcast for the candidate should be accepted"
1571            );
1572            context.sleep(Duration::from_millis(10)).await;
1573
1574            // The equivocating proposal names the certified view-1 block as
1575            // parent, which this validator's parent selection accepts
1576            // (view 1 certified, view 2 nullified). Refusing to notarize it
1577            // is correct.
1578            let equivocating_ctx = Ctx {
1579                round,
1580                leader,
1581                parent: (View::new(1), certified_digest),
1582            };
1583            let verify_rx = inline.verify(equivocating_ctx, digest).await;
1584            assert!(
1585                !verify_rx.await.expect("verify result missing"),
1586                "the equivocating proposal must not be notarized"
1587            );
1588
1589            // The honest notarization for the same `(round, digest)` arrives.
1590            let certify_rx = inline.certify(round, digest).await;
1591            select! {
1592                result = certify_rx => {
1593                    assert!(
1594                        result.expect("certify result missing"),
1595                        "certify of the notarized digest must not adopt the verdict computed under the equivocating context"
1596                    );
1597                },
1598                _ = context.sleep(Duration::from_secs(5)) => {
1599                    panic!("certify should resolve promptly");
1600                },
1601            }
1602        });
1603    }
1604
1605    impl Inline<deterministic::Context, S, MockVerifyingApp<B, S>, B, FixedEpocher> {
1606        /// Exposes the gates this wrapper prunes to the wrapper tests shared by the standard
1607        /// variants.
1608        pub(in crate::marshal::standard) const fn gates(&self) -> &Gates<D, Digested<B>> {
1609            &self.gates
1610        }
1611    }
1612}