Skip to main content

commonware_consensus/marshal/coding/
mod.rs

1//! Ordered delivery of erasure-coded blocks.
2//!
3//! # Overview
4//!
5//! The coding marshal couples the consensus pipeline with erasure-coded block broadcast.
6//! Blocks are produced by an application, encoded into [`types::Shard`]s, fanned out to peers, and
7//! later reconstructed when a notarization or finalization proves that the data is needed.
8//! Compared to [`super::standard`], this variant makes more efficient usage of the network's bandwidth
9//! by spreading the load of block dissemination across all participants.
10//!
11//! # Components
12//!
13//! - [`crate::marshal::core::Actor`]: The unified marshal actor that orders finalized blocks,
14//!   handles acknowledgements from the application, and requests repairs when gaps are detected.
15//!   Used with [`Coding`] as the variant type parameter.
16//! - [`crate::marshal::core::Mailbox`]: Accepts requests from other local subsystems and forwards
17//!   them to the actor. Used with [`Coding`] as the variant type parameter.
18//! - [`shards::Engine`]: Broadcasts shards, verifies locally held fragments, and reconstructs
19//!   entire [`types::CodedBlock`]s on demand.
20//! - [`crate::marshal::resolver`]: Issues outbound fetches to remote peers when marshal is missing
21//!   a block, notarization, or finalization referenced by consensus.
22//! - [`types`]: Defines commitments, distribution shards, and helper builders used across the
23//!   module.
24//! - [`Marshaled`]: Wraps an [`crate::Application`] implementation so it automatically enforces
25//!   epoch boundaries and performs erasure encoding before a proposal leaves the application.
26//!
27//! # Data Flow
28//!
29//! 1. The application produces a block through [`Marshaled`], which encodes the payload and
30//!    obtains a [`crate::types::coding::Commitment`] describing the shard layout.
31//! 2. The block is broadcast via [`shards::Engine`]; each participant receives exactly one shard
32//!    and reshares it to everyone else once it verifies the fragment.
33//! 3. The actor ingests notarizations/finalizations from `simplex`, pulls reconstructed blocks
34//!    from the shard engine or backfills them through [`crate::marshal::resolver`], and durably
35//!    persists the ordered data.
36//! 4. The actor reports finalized blocks to the node's [`crate::Reporter`] at-least-once and
37//!    drives repair loops whenever notarizations reference yet-to-be-delivered payloads.
38//!
39//! # Storage and Repair
40//!
41//! Notarized data and certificates live in prunable archives managed internally, while finalized
42//! blocks are migrated into immutable archives. Any gaps are filled by asking peers for specific
43//! commitments through the resolver pipeline. The shard engine keeps only ephemeral, in-memory
44//! caches; once a block is finalized it is evicted from the reconstruction map, reducing memory
45//! pressure.
46//!
47//! # When to Use
48//!
49//! Choose this module when the consensus deployment wants erasure-coded dissemination with the
50//! same ordering guarantees provided by [`super::standard`]. The API is a breaking change from
51//! the standard marshal: applications must adapt to the coding-specific variant type and buffer
52//! implementation required by this module.
53
54pub mod shards;
55pub mod types;
56pub(crate) mod validation;
57
58mod variant;
59pub use variant::Coding;
60
61mod marshaled;
62pub use marshaled::{Marshaled, MarshaledConfig};
63
64#[cfg(test)]
65mod tests {
66    use crate::{
67        marshal::{
68            ancestry::BlockProvider,
69            coding::{
70                shards,
71                types::{coding_config_for_participants, hash_context, CodedBlock},
72                Coding, Marshaled, MarshaledConfig,
73            },
74            config::{Config, Start},
75            core,
76            mocks::{
77                application::Application,
78                harness::{
79                    self, default_leader, genesis_commitment, make_coding_block,
80                    setup_network_links, setup_network_with_participants, CodingB, CodingCtx,
81                    CodingHarness, EmptyProvider, TestHarness, BLOCKS_PER_EPOCH, D, K, LINK,
82                    NAMESPACE, NUM_VALIDATORS, QUORUM, S, TEST_QUOTA, UNRELIABLE_LINK, V,
83                },
84                verifying::{GatedVerifyingApp, MockVerifyingApp},
85            },
86            resolver::handler,
87        },
88        simplex::{
89            scheme::bls12381_threshold::vrf as bls12381_threshold_vrf, types::Proposal, Plan,
90        },
91        types::{coding::Commitment, Epoch, Epocher, FixedEpocher, Height, Round, View, ViewDelta},
92        Automaton, Block, CertifiableAutomaton, CertifiableBlock, Relay,
93    };
94    use bytes::Bytes;
95    use commonware_actor::{mailbox, Feedback};
96    use commonware_codec::{Encode, FixedSize};
97    use commonware_coding::{CodecConfig, Config as CodingConfig, ReedSolomon};
98    use commonware_cryptography::{
99        certificate::{mocks::Fixture, ConstantProvider, Verifier as _},
100        sha256::Sha256,
101        Committable, Digestible, Hasher,
102    };
103    use commonware_macros::{select, test_group, test_traced};
104    use commonware_p2p::Recipients;
105    use commonware_parallel::Sequential;
106    use commonware_resolver::{Delivery, Fetch, Resolver, TargetedResolver};
107    use commonware_runtime::{
108        buffer::paged::CacheRef, deterministic, Clock, Metrics, Runner, Supervisor as _,
109    };
110    use commonware_storage::archive::immutable;
111    use commonware_utils::{
112        channel::oneshot, sync::Mutex, vec::NonEmptyVec, NZUsize, NZU16, NZU64,
113    };
114    use std::{sync::Arc, time::Duration};
115
116    type TestCodingVariant = Coding<CodingB, ReedSolomon<Sha256>, Sha256, K>;
117    type TestCodedBlock = CodedBlock<CodingB, ReedSolomon<Sha256>, Sha256>;
118    type CodingSendRecord = (Round, Arc<TestCodedBlock>, Recipients<K>);
119
120    // Smallest valid coding config used to build trusted genesis commitments.
121    const GENESIS_CODING_CONFIG: CodingConfig = CodingConfig {
122        minimum_shards: NZU16!(1),
123        extra_shards: NZU16!(1),
124    };
125
126    #[test]
127    fn mailbox_provides_application_blocks() {
128        fn assert_provider<P: BlockProvider<Block = CodingB>>() {}
129        assert_provider::<core::Mailbox<S, TestCodingVariant>>();
130    }
131
132    /// A coding buffer that records subscriptions and never resolves them.
133    #[derive(Clone, Default)]
134    struct RecordingCodingBuffer {
135        digest_subscriptions: Arc<Mutex<Vec<oneshot::Sender<Arc<TestCodedBlock>>>>>,
136        commitment_subscriptions: Arc<Mutex<Vec<oneshot::Sender<Arc<TestCodedBlock>>>>>,
137        sends: Arc<Mutex<Vec<CodingSendRecord>>>,
138    }
139
140    impl RecordingCodingBuffer {
141        fn subscription_count(&self) -> usize {
142            self.digest_subscriptions.lock().len() + self.commitment_subscriptions.lock().len()
143        }
144
145        fn commitment_subscription_count(&self) -> usize {
146            self.commitment_subscriptions.lock().len()
147        }
148    }
149
150    impl core::Buffer<TestCodingVariant> for RecordingCodingBuffer {
151        type PublicKey = K;
152
153        async fn find_by_digest(&self, _digest: D) -> Option<Arc<TestCodedBlock>> {
154            None
155        }
156
157        async fn find_by_commitment(&self, _commitment: Commitment) -> Option<Arc<TestCodedBlock>> {
158            None
159        }
160
161        fn subscribe_by_digest(
162            &self,
163            _digest: D,
164        ) -> Option<oneshot::Receiver<Arc<TestCodedBlock>>> {
165            let (sender, receiver) = oneshot::channel();
166            self.digest_subscriptions.lock().push(sender);
167            Some(receiver)
168        }
169
170        fn subscribe_by_commitment(
171            &self,
172            _commitment: Commitment,
173        ) -> Option<oneshot::Receiver<Arc<TestCodedBlock>>> {
174            let (sender, receiver) = oneshot::channel();
175            self.commitment_subscriptions.lock().push(sender);
176            Some(receiver)
177        }
178
179        fn finalized(&self, _commitment: Commitment) {}
180
181        fn send(&self, round: Round, block: Arc<TestCodedBlock>, recipients: Recipients<K>) {
182            self.sends.lock().push((round, block, recipients));
183        }
184    }
185
186    type CodingFetchRecord = Fetch<handler::Key<Commitment>, handler::Annotation>;
187    type CodingTargetedFetch = (handler::Key<Commitment>, NonEmptyVec<K>);
188
189    /// A resolver that records each fetch invocation; other methods are no-ops.
190    #[derive(Clone, Default)]
191    struct RecordingResolver {
192        fetches: Arc<Mutex<Vec<CodingFetchRecord>>>,
193        targeted: Arc<Mutex<Vec<CodingTargetedFetch>>>,
194        auto_delivery: Arc<Mutex<Option<Bytes>>>,
195        delivery_responses: Arc<Mutex<Vec<oneshot::Receiver<bool>>>>,
196        sender: Option<mailbox::Sender<handler::Message<Commitment>>>,
197    }
198
199    impl RecordingResolver {
200        fn holding(metrics: impl Metrics) -> (handler::Receiver<Commitment>, Self) {
201            let (sender, receiver) = mailbox::new(metrics, NZUsize!(100));
202            (
203                handler::Receiver::new(receiver),
204                Self {
205                    fetches: Arc::new(Mutex::new(Vec::new())),
206                    targeted: Arc::new(Mutex::new(Vec::new())),
207                    auto_delivery: Arc::new(Mutex::new(None)),
208                    delivery_responses: Arc::new(Mutex::new(Vec::new())),
209                    sender: Some(sender),
210                },
211            )
212        }
213
214        fn record_fetch(&self, fetch: CodingFetchRecord) {
215            self.fetches.lock().push(fetch.clone());
216            let Some(value) = self.auto_delivery.lock().take() else {
217                return;
218            };
219            let Some(sender) = &self.sender else {
220                return;
221            };
222            let (response, response_rx) = oneshot::channel();
223            self.delivery_responses.lock().push(response_rx);
224            let _ = sender.enqueue(handler::Message::Deliver {
225                delivery: Delivery {
226                    key: fetch.key,
227                    subscribers: NonEmptyVec::new((fetch.subscriber, tracing::Span::none())),
228                },
229                value,
230                response,
231            });
232        }
233
234        fn respond_to_next_fetch(&self, value: Bytes) {
235            let replaced = self.auto_delivery.lock().replace(value);
236            assert!(
237                replaced.is_none(),
238                "recording resolver already has an automatic delivery"
239            );
240        }
241
242        async fn wait_for_delivery_response(&self) -> bool {
243            let response = self
244                .delivery_responses
245                .lock()
246                .pop()
247                .expect("delivery response missing");
248            response.await.expect("delivery response sender dropped")
249        }
250
251        fn fetches(&self) -> Vec<CodingFetchRecord> {
252            self.fetches.lock().clone()
253        }
254
255        fn targeted(&self) -> Vec<CodingTargetedFetch> {
256            self.targeted.lock().clone()
257        }
258    }
259
260    impl Resolver for RecordingResolver {
261        type Key = handler::Key<Commitment>;
262        type Subscriber = handler::Annotation;
263
264        fn fetch<F>(&mut self, fetch: F) -> Feedback
265        where
266            F: Into<Fetch<Self::Key, Self::Subscriber>> + Send,
267        {
268            self.record_fetch(fetch.into());
269            Feedback::Ok
270        }
271
272        fn fetch_all<F>(&mut self, fetches: Vec<F>) -> Feedback
273        where
274            F: Into<Fetch<Self::Key, Self::Subscriber>> + Send,
275        {
276            for fetch in fetches {
277                self.record_fetch(fetch.into());
278            }
279            Feedback::Ok
280        }
281
282        fn retain(
283            &mut self,
284            _predicate: impl Fn(&Self::Key, &Self::Subscriber) -> bool + Send + 'static,
285        ) -> Feedback {
286            Feedback::Ok
287        }
288    }
289
290    impl TargetedResolver for RecordingResolver {
291        type PublicKey = K;
292
293        fn fetch_targeted(
294            &mut self,
295            fetch: impl Into<Fetch<Self::Key, Self::Subscriber>> + Send,
296            targets: NonEmptyVec<Self::PublicKey>,
297        ) -> Feedback {
298            self.targeted.lock().push((fetch.into().key, targets));
299            Feedback::Ok
300        }
301
302        fn fetch_all_targeted<F>(
303            &mut self,
304            fetches: Vec<(F, NonEmptyVec<Self::PublicKey>)>,
305        ) -> Feedback
306        where
307            F: Into<Fetch<Self::Key, Self::Subscriber>> + Send,
308        {
309            let mut targeted = self.targeted.lock();
310            for (fetch, targets) in fetches {
311                targeted.push((fetch.into().key, targets));
312            }
313            Feedback::Ok
314        }
315    }
316
317    async fn start_coding_actor_with_recording(
318        context: deterministic::Context,
319        partition_prefix: &str,
320        provider: ConstantProvider<S, Epoch>,
321        buffer: RecordingCodingBuffer,
322    ) -> (
323        core::Mailbox<S, TestCodingVariant>,
324        RecordingResolver,
325        commonware_runtime::Handle<()>,
326    ) {
327        let config = Config {
328            provider,
329            epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
330            start: Start::Genesis(CodingHarness::genesis_block(NUM_VALIDATORS as u16)),
331            mailbox_size: NZUsize!(100),
332            view_retention_timeout: ViewDelta::new(10),
333            max_repair: NZUsize!(10),
334            max_pending_acks: NZUsize!(1),
335            block_codec_config: (),
336            partition_prefix: partition_prefix.to_string(),
337            prunable_items_per_section: NZU64!(10),
338            replay_buffer: NZUsize!(1024),
339            key_write_buffer: NZUsize!(1024),
340            value_write_buffer: NZUsize!(1024),
341            page_cache: CacheRef::from_pooler(
342                &context,
343                harness::PAGE_SIZE,
344                harness::PAGE_CACHE_SIZE,
345            ),
346            strategy: Sequential,
347        };
348
349        let finalizations_by_height = immutable::Archive::init(
350            context.child("finalizations_by_height"),
351            immutable::Config {
352                metadata_partition: format!("{partition_prefix}-finalizations-by-height-metadata"),
353                freezer_table_partition: format!(
354                    "{partition_prefix}-finalizations-by-height-freezer-table"
355                ),
356                freezer_table_initial_size: 64,
357                freezer_table_resize_frequency: 10,
358                freezer_table_resize_chunk_size: 10,
359                freezer_key_partition: format!(
360                    "{partition_prefix}-finalizations-by-height-freezer-key"
361                ),
362                freezer_key_page_cache: config.page_cache.clone(),
363                freezer_value_partition: format!(
364                    "{partition_prefix}-finalizations-by-height-freezer-value"
365                ),
366                freezer_value_target_size: 1024,
367                freezer_value_compression: None,
368                ordinal_partition: format!("{partition_prefix}-finalizations-by-height-ordinal"),
369                items_per_section: NZU64!(10),
370                codec_config: S::certificate_codec_config_unbounded(),
371                replay_buffer: config.replay_buffer,
372                freezer_key_write_buffer: config.key_write_buffer,
373                freezer_value_write_buffer: config.value_write_buffer,
374                ordinal_write_buffer: config.key_write_buffer,
375            },
376        )
377        .await
378        .expect("failed to initialize finalizations by height archive");
379
380        let finalized_blocks = immutable::Archive::init(
381            context.child("finalized_blocks"),
382            immutable::Config {
383                metadata_partition: format!("{partition_prefix}-finalized_blocks-metadata"),
384                freezer_table_partition: format!(
385                    "{partition_prefix}-finalized_blocks-freezer-table"
386                ),
387                freezer_table_initial_size: 64,
388                freezer_table_resize_frequency: 10,
389                freezer_table_resize_chunk_size: 10,
390                freezer_key_partition: format!("{partition_prefix}-finalized_blocks-freezer-key"),
391                freezer_key_page_cache: config.page_cache.clone(),
392                freezer_value_partition: format!(
393                    "{partition_prefix}-finalized_blocks-freezer-value"
394                ),
395                freezer_value_target_size: 1024,
396                freezer_value_compression: None,
397                ordinal_partition: format!("{partition_prefix}-finalized_blocks-ordinal"),
398                items_per_section: NZU64!(10),
399                codec_config: config.block_codec_config,
400                replay_buffer: config.replay_buffer,
401                freezer_key_write_buffer: config.key_write_buffer,
402                freezer_value_write_buffer: config.value_write_buffer,
403                ordinal_write_buffer: config.key_write_buffer,
404            },
405        )
406        .await
407        .expect("failed to initialize finalized blocks archive");
408
409        let (actor, mailbox, _) = core::Actor::init(
410            context.child("actor"),
411            finalizations_by_height,
412            finalized_blocks,
413            config,
414        )
415        .await;
416        let (resolver_rx, resolver) = RecordingResolver::holding(context.child("resolver"));
417        let actor_handle = actor.start(
418            Application::<CodingB>::default(),
419            buffer,
420            (resolver_rx, resolver.clone()),
421        );
422        (mailbox, resolver, actor_handle)
423    }
424
425    async fn start_shard_mailbox(
426        context: deterministic::Context,
427        participants: Vec<K>,
428        provider: ConstantProvider<S, Epoch>,
429    ) -> shards::Mailbox<CodingB, ReedSolomon<Sha256>, Sha256, K> {
430        let me = participants[0].clone();
431        let oracle =
432            setup_network_with_participants(context.child("network"), NZUsize!(1), participants)
433                .await;
434        let control = oracle.control(me.clone());
435        let shard_config: shards::Config<_, _, _, _, _, Sha256, _, _> = shards::Config {
436            scheme_provider: provider,
437            blocker: control.clone(),
438            shard_codec_cfg: CodecConfig {
439                maximum_shard_size: 1024 * 1024,
440            },
441            block_codec_cfg: (),
442            strategy: Sequential,
443            mailbox_size: NZUsize!(10),
444            peer_buffer_size: NZUsize!(64),
445            background_channel_capacity: NZUsize!(1024),
446            peer_provider: oracle.manager(),
447        };
448        let (shard_engine, shard_mailbox) =
449            shards::Engine::new(context.child("shards"), shard_config);
450        let network = control.register(0, TEST_QUOTA).await.unwrap();
451        shard_engine.start(network);
452        shard_mailbox
453    }
454
455    fn genesis_block() -> CodingB {
456        let genesis_ctx = CodingCtx {
457            round: Round::zero(),
458            leader: default_leader(),
459            parent: (View::zero(), genesis_commitment()),
460        };
461        make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0)
462    }
463
464    fn genesis_coding_commitment<H: Hasher, B: CertifiableBlock>(block: &B) -> Commitment {
465        Commitment::from((
466            block.digest(),
467            block.digest(),
468            hash_context::<H, _>(&block.context()),
469            GENESIS_CODING_CONFIG,
470        ))
471    }
472
473    fn missing_candidate(me: K) -> (CodingCtx, TestCodedBlock) {
474        let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
475        let genesis = genesis_block();
476        let genesis_parent_commitment = genesis_coding_commitment::<Sha256, _>(&genesis);
477        let round = Round::new(Epoch::zero(), View::new(1));
478        let candidate_ctx = CodingCtx {
479            round,
480            leader: me,
481            parent: (View::zero(), genesis_parent_commitment),
482        };
483        let candidate =
484            make_coding_block(candidate_ctx.clone(), genesis.digest(), Height::new(1), 100);
485        let coded_candidate: TestCodedBlock =
486            CodedBlock::new(candidate, coding_config, &Sequential);
487        (candidate_ctx, coded_candidate)
488    }
489
490    #[test_traced("WARN")]
491    fn test_coding_notarized_delivery_rejects_dishonest_payload_config() {
492        let runner = deterministic::Runner::timed(Duration::from_secs(30));
493        runner.start(|mut context| async move {
494            let Fixture {
495                participants,
496                schemes,
497                ..
498            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
499            let provider = ConstantProvider::new(schemes[0].clone());
500            let honest_config = coding_config_for_participants(NUM_VALIDATORS as u16);
501            let dishonest_config = coding_config_for_participants((NUM_VALIDATORS + 3) as u16);
502            assert_ne!(honest_config, dishonest_config);
503
504            let buffer = RecordingCodingBuffer::default();
505            let (marshal, resolver, _actor_handle) = start_coding_actor_with_recording(
506                context.child("actor_stack"),
507                "coding-dishonest-payload-config",
508                provider,
509                buffer,
510            )
511            .await;
512            let resolver_tx = resolver
513                .sender
514                .clone()
515                .expect("recording resolver should keep its sender");
516
517            let genesis = genesis_block();
518            let round = Round::new(Epoch::zero(), View::new(1));
519            let height = Height::new(1);
520            let candidate_ctx = CodingCtx {
521                round,
522                leader: participants[0].clone(),
523                parent: (
524                    View::zero(),
525                    genesis_coding_commitment::<Sha256, _>(&genesis),
526                ),
527            };
528            let candidate = make_coding_block(candidate_ctx, genesis.digest(), height, 100);
529            let dishonest_block: TestCodedBlock =
530                CodedBlock::new(candidate.clone(), dishonest_config, &Sequential);
531            let proposal = Proposal {
532                round,
533                parent: View::zero(),
534                payload: dishonest_block.commitment(),
535            };
536            let notarization = CodingHarness::make_notarization(proposal, &schemes, QUORUM);
537
538            let (response, response_rx) = oneshot::channel();
539            assert!(resolver_tx
540                .enqueue(handler::Message::Deliver {
541                    delivery: Delivery {
542                        key: handler::Key::Notarized { round },
543                        subscribers: NonEmptyVec::new((
544                            handler::Annotation::Notarization { round },
545                            tracing::Span::none(),
546                        )),
547                    },
548                    value: (notarization, dishonest_block).encode(),
549                    response,
550                })
551                .accepted());
552            assert!(
553                !response_rx.await.unwrap(),
554                "notarized delivery should reject a dishonest coding config"
555            );
556
557            context.sleep(Duration::from_millis(100)).await;
558            assert!(
559                marshal.get_block(height).await.is_none(),
560                "dishonest deliveries must not store a finalized block"
561            );
562            assert!(
563                marshal.get_finalization(height).await.is_none(),
564                "dishonest deliveries must not archive a finalization"
565            );
566        });
567    }
568
569    #[test_traced("WARN")]
570    fn test_coding_block_provider_parent_fetches_by_commitment() {
571        let runner = deterministic::Runner::timed(Duration::from_secs(30));
572        runner.start(|mut context| async move {
573            let Fixture {
574                participants,
575                schemes,
576                ..
577            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
578            let provider = ConstantProvider::new(schemes[0].clone());
579            let buffer = RecordingCodingBuffer::default();
580            let (marshal, _resolver, _actor_handle) = start_coding_actor_with_recording(
581                context.child("actor_stack"),
582                "coding-provider-parent-commitment",
583                provider,
584                buffer.clone(),
585            )
586            .await;
587
588            let (parent_ctx, parent) = missing_candidate(participants[0].clone());
589            let child_ctx = CodingCtx {
590                round: Round::new(Epoch::zero(), View::new(2)),
591                leader: participants[0].clone(),
592                parent: (parent_ctx.round.view(), parent.commitment()),
593            };
594            let child = make_coding_block(child_ctx, parent.digest(), Height::new(2), 200);
595            let subscription = marshal.subscribe_parent(&child);
596
597            context.sleep(Duration::from_millis(100)).await;
598            assert_eq!(
599                buffer.commitment_subscription_count(),
600                1,
601                "parent walkback should use the coding parent commitment"
602            );
603            drop(subscription);
604        });
605    }
606
607    #[test_traced("WARN")]
608    fn test_coding_verify_missing_candidate_waits_without_fetching() {
609        let runner = deterministic::Runner::timed(Duration::from_secs(30));
610        runner.start(|mut context| async move {
611            let Fixture {
612                participants,
613                schemes,
614                ..
615            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
616            let provider = ConstantProvider::new(schemes[0].clone());
617            let me = participants[0].clone();
618            let buffer = RecordingCodingBuffer::default();
619            let (marshal, resolver, _actor_handle) = start_coding_actor_with_recording(
620                context.child("actor_stack"),
621                "coding-verify-missing-candidate",
622                provider.clone(),
623                buffer.clone(),
624            )
625            .await;
626            let shards =
627                start_shard_mailbox(context.child("shard_stack"), participants, provider.clone())
628                    .await;
629            let (candidate_ctx, candidate) = missing_candidate(me);
630            let commitment = candidate.commitment();
631
632            let cfg = MarshaledConfig {
633                application: MockVerifyingApp::<CodingB, S>::new(),
634                marshal,
635                shards,
636                scheme_provider: provider,
637                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
638                strategy: Sequential,
639            };
640            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
641
642            let verify_rx = marshaled.verify(candidate_ctx, commitment).await;
643            context.sleep(Duration::from_millis(100)).await;
644
645            assert!(
646                buffer.subscription_count() > 0,
647                "missing candidate should register a local buffer wait"
648            );
649            assert!(
650                resolver.fetches().is_empty(),
651                "missing candidate verify must not fetch from peers"
652            );
653            assert!(
654                resolver.targeted().is_empty(),
655                "missing candidate verify must not issue targeted fetches"
656            );
657            drop(verify_rx);
658        });
659    }
660
661    /// Crash-recovery shape: after an unclean shutdown, Simplex may recover a
662    /// notarized commitment while marshal has no local certification gate task and no
663    /// durable block. If enough shards or a peer response can provide the block,
664    /// certification should fetch it by notarized round and persist it instead
665    /// of treating the missing local copy as a hard failure.
666    #[test_traced("WARN")]
667    fn test_coding_certify_missing_candidate_fetches_by_round() {
668        let runner = deterministic::Runner::timed(Duration::from_secs(30));
669        runner.start(|mut context| async move {
670            let Fixture {
671                participants,
672                schemes,
673                ..
674            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
675            let provider = ConstantProvider::new(schemes[0].clone());
676            let me = participants[0].clone();
677            let buffer = RecordingCodingBuffer::default();
678            let (marshal, resolver, _actor_handle) = start_coding_actor_with_recording(
679                context.child("actor_stack"),
680                "coding-certify-missing-candidate",
681                provider.clone(),
682                buffer.clone(),
683            )
684            .await;
685            let shards =
686                start_shard_mailbox(context.child("shard_stack"), participants, provider.clone())
687                    .await;
688
689            let cfg = MarshaledConfig {
690                application: MockVerifyingApp::<CodingB, S>::new(),
691                marshal,
692                shards,
693                scheme_provider: provider,
694                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
695                strategy: Sequential,
696            };
697            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
698
699            let (candidate_ctx, candidate) = missing_candidate(me);
700            let commitment = candidate.commitment();
701            let round = candidate_ctx.round;
702            let proposal = Proposal::new(round, View::zero(), commitment);
703            let notarization = CodingHarness::make_notarization(proposal, &schemes, QUORUM);
704            resolver.respond_to_next_fetch((notarization, candidate).encode());
705            let certify_rx = marshaled.certify(round, commitment).await;
706
707            let result = certify_rx.await.expect("certify result missing");
708            assert!(result, "fetched notarized candidate should certify");
709            assert!(
710                resolver.wait_for_delivery_response().await,
711                "notarized delivery should validate"
712            );
713            assert!(
714                resolver.fetches().iter().any(|fetch| matches!(
715                    (&fetch.key, &fetch.subscriber),
716                    (
717                        handler::Key::Notarized { round: request_round },
718                        handler::Annotation::Notarization { round: subscriber_round },
719                    ) if *request_round == round && *subscriber_round == round
720                )),
721                "certify should fetch notarized block by round"
722            );
723
724            assert!(
725                buffer.subscription_count() > 0,
726                "missing candidate should register a local buffer wait"
727            );
728            assert!(
729                resolver.targeted().is_empty(),
730                "missing candidate certify must not issue targeted fetches"
731            );
732        });
733    }
734
735    #[test_traced("WARN")]
736    fn test_coding_certify_pending_verify_fetches_by_round() {
737        let runner = deterministic::Runner::timed(Duration::from_secs(30));
738        runner.start(|mut context| async move {
739            let Fixture {
740                participants,
741                schemes,
742                ..
743            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
744            let provider = ConstantProvider::new(schemes[0].clone());
745            let me = participants[0].clone();
746            let buffer = RecordingCodingBuffer::default();
747            let (marshal, resolver, _actor_handle) = start_coding_actor_with_recording(
748                context.child("actor_stack"),
749                "coding-certify-pending-verify",
750                provider.clone(),
751                buffer,
752            )
753            .await;
754            let shards =
755                start_shard_mailbox(context.child("shard_stack"), participants, provider.clone())
756                    .await;
757
758            let cfg = MarshaledConfig {
759                application: MockVerifyingApp::<CodingB, S>::new(),
760                marshal,
761                shards,
762                scheme_provider: provider,
763                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
764                strategy: Sequential,
765            };
766            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
767
768            let (candidate_ctx, candidate) = missing_candidate(me);
769            let commitment = candidate.commitment();
770            let round = candidate_ctx.round;
771            let _verify_rx = marshaled.verify(candidate_ctx, commitment).await;
772
773            let proposal = Proposal::new(round, View::zero(), commitment);
774            let notarization = CodingHarness::make_notarization(proposal, &schemes, QUORUM);
775            resolver.respond_to_next_fetch((notarization, candidate).encode());
776            let certify_rx = marshaled.certify(round, commitment).await;
777
778            let result = certify_rx.await.expect("certify result missing");
779            assert!(
780                result,
781                "pending verify should complete after certification recovery"
782            );
783            assert!(
784                resolver.wait_for_delivery_response().await,
785                "notarized delivery should validate"
786            );
787            assert!(
788                resolver.fetches().iter().any(|fetch| matches!(
789                    (&fetch.key, &fetch.subscriber),
790                    (
791                        handler::Key::Notarized { round: request_round },
792                        handler::Annotation::Notarization { round: subscriber_round },
793                    ) if *request_round == round && *subscriber_round == round
794                )),
795                "certify should recover a pending verify by notarized round"
796            );
797            assert!(
798                resolver.targeted().is_empty(),
799                "certify recovery must not issue targeted fetches"
800            );
801        });
802    }
803
804    #[test_group("slow")]
805    #[test_traced("WARN")]
806    fn test_coding_finalize_good_links() {
807        for seed in 0..5 {
808            let r1 = harness::finalize::<CodingHarness>(seed, LINK, false);
809            let r2 = harness::finalize::<CodingHarness>(seed, LINK, false);
810            assert_eq!(r1, r2);
811        }
812    }
813
814    #[test_group("slow")]
815    #[test_traced("WARN")]
816    fn test_coding_finalize_bad_links() {
817        for seed in 0..5 {
818            let r1 = harness::finalize::<CodingHarness>(seed, UNRELIABLE_LINK, false);
819            let r2 = harness::finalize::<CodingHarness>(seed, UNRELIABLE_LINK, false);
820            assert_eq!(r1, r2);
821        }
822    }
823
824    #[test_group("slow")]
825    #[test_traced("WARN")]
826    fn test_coding_finalize_good_links_quorum_sees_finalization() {
827        for seed in 0..5 {
828            let r1 = harness::finalize::<CodingHarness>(seed, LINK, true);
829            let r2 = harness::finalize::<CodingHarness>(seed, LINK, true);
830            assert_eq!(r1, r2);
831        }
832    }
833
834    #[test_group("slow")]
835    #[test_traced("WARN")]
836    fn test_coding_finalize_bad_links_quorum_sees_finalization() {
837        for seed in 0..5 {
838            let r1 = harness::finalize::<CodingHarness>(seed, UNRELIABLE_LINK, true);
839            let r2 = harness::finalize::<CodingHarness>(seed, UNRELIABLE_LINK, true);
840            assert_eq!(r1, r2);
841        }
842    }
843
844    #[test_group("slow")]
845    #[test_traced("WARN")]
846    fn test_coding_hailstorm_restarts() {
847        for seed in 0..2 {
848            let r1 = harness::hailstorm::<CodingHarness>(seed, 4, 4, 1, LINK);
849            let r2 = harness::hailstorm::<CodingHarness>(seed, 4, 4, 1, LINK);
850            assert_eq!(r1, r2);
851        }
852    }
853
854    #[test_group("slow")]
855    #[test_traced("WARN")]
856    fn test_coding_hailstorm_multi_restarts() {
857        for seed in 0..2 {
858            let r1 = harness::hailstorm::<CodingHarness>(seed, 4, 4, 2, LINK);
859            let r2 = harness::hailstorm::<CodingHarness>(seed, 4, 4, 2, LINK);
860            assert_eq!(r1, r2);
861        }
862    }
863
864    #[test_traced("WARN")]
865    fn test_coding_ack_pipeline_backlog() {
866        harness::ack_pipeline_backlog::<CodingHarness>();
867    }
868
869    #[test_traced("WARN")]
870    fn test_coding_ack_pipeline_backlog_persists_on_restart() {
871        harness::ack_pipeline_backlog_persists_on_restart::<CodingHarness>();
872    }
873
874    #[test_traced("WARN")]
875    fn test_coding_genesis_emitted_once() {
876        harness::genesis_emitted_once::<CodingHarness>();
877    }
878
879    #[test_traced("WARN")]
880    fn test_coding_proposed_success_implies_recoverable_after_restart() {
881        harness::proposed_success_implies_recoverable_after_restart::<CodingHarness>(0..16);
882    }
883
884    #[test_traced("WARN")]
885    fn test_coding_verified_success_implies_recoverable_after_restart() {
886        harness::verified_success_implies_recoverable_after_restart::<CodingHarness>(0..16);
887    }
888
889    #[test_traced("WARN")]
890    fn test_coding_certified_success_implies_recoverable_after_restart() {
891        harness::certified_success_implies_recoverable_after_restart::<CodingHarness>(0..16);
892    }
893
894    #[test_traced("WARN")]
895    fn test_coding_delivery_visibility_implies_recoverable_after_restart() {
896        harness::delivery_visibility_implies_recoverable_after_restart::<CodingHarness>(0..16);
897    }
898
899    #[test_traced("WARN")]
900    fn test_coding_sync_height_floor() {
901        harness::sync_height_floor::<CodingHarness>();
902    }
903
904    #[test_traced("WARN")]
905    fn test_coding_prune_finalized_archives() {
906        harness::prune_finalized_archives::<CodingHarness>();
907    }
908
909    #[test_traced("WARN")]
910    fn test_coding_rejects_block_delivery_below_floor() {
911        harness::reject_stale_block_delivery_after_floor_update::<CodingHarness>();
912    }
913
914    #[test_traced("WARN")]
915    fn test_coding_commitment_fetch_height_hint_mismatch_wakes_subscriber() {
916        harness::commitment_fetch_height_hint_mismatch_wakes_subscriber::<CodingHarness>();
917    }
918
919    #[test_traced("WARN")]
920    fn test_coding_subscribe_basic_block_delivery() {
921        harness::subscribe_basic_block_delivery::<CodingHarness>();
922    }
923
924    #[test_traced("WARN")]
925    fn test_coding_subscribe_multiple_subscriptions() {
926        harness::subscribe_multiple_subscriptions::<CodingHarness>();
927    }
928
929    #[test_traced("WARN")]
930    fn test_coding_subscribe_canceled_subscriptions() {
931        harness::subscribe_canceled_subscriptions::<CodingHarness>();
932    }
933
934    #[test_traced("WARN")]
935    fn test_coding_subscribe_blocks_from_different_sources() {
936        harness::subscribe_blocks_from_different_sources::<CodingHarness>();
937    }
938
939    #[test_traced("WARN")]
940    fn test_coding_get_info_basic_queries_present_and_missing() {
941        harness::get_info_basic_queries_present_and_missing::<CodingHarness>();
942    }
943
944    #[test_traced("WARN")]
945    fn test_coding_get_info_latest_progression_multiple_finalizations() {
946        harness::get_info_latest_progression_multiple_finalizations::<CodingHarness>();
947    }
948
949    #[test_traced("WARN")]
950    fn test_coding_get_block_by_height_and_latest() {
951        harness::get_block_by_height_and_latest::<CodingHarness>();
952    }
953
954    #[test_traced("WARN")]
955    fn test_coding_get_block_by_commitment_from_sources_and_missing() {
956        harness::get_block_by_commitment_from_sources_and_missing::<CodingHarness>();
957    }
958
959    #[test_traced("WARN")]
960    fn test_coding_get_finalization_by_height() {
961        harness::get_finalization_by_height::<CodingHarness>();
962    }
963
964    #[test_traced("WARN")]
965    fn test_coding_hint_finalized_triggers_fetch() {
966        harness::hint_finalized_triggers_fetch::<CodingHarness>();
967    }
968
969    #[test_traced("WARN")]
970    fn test_coding_ancestry_stream() {
971        harness::ancestry_stream::<CodingHarness>();
972    }
973
974    #[test_traced("WARN")]
975    fn test_coding_finalize_same_height_different_views() {
976        harness::finalize_same_height_different_views::<CodingHarness>();
977    }
978
979    #[test_traced("WARN")]
980    fn test_coding_certify_persists_equivocated_block() {
981        harness::certify_persists_equivocated_block::<CodingHarness>();
982    }
983
984    #[test_traced("WARN")]
985    fn test_coding_verified_after_restart_reverify_same_round_implies_recoverable() {
986        harness::verified_after_restart_reverify_same_round_implies_recoverable::<CodingHarness>();
987    }
988
989    #[test_traced("WARN")]
990    fn test_coding_certify_after_restart_reverify_same_round_implies_recoverable() {
991        harness::certify_after_restart_reverify_same_round_implies_recoverable::<CodingHarness>();
992    }
993
994    #[test_traced("WARN")]
995    fn test_coding_certify_at_later_view_survives_earlier_view_pruning() {
996        harness::certify_at_later_view_survives_earlier_view_pruning::<CodingHarness>();
997    }
998
999    #[test_traced("WARN")]
1000    fn test_coding_certify_first_block_fetches_genesis_parent() {
1001        let runner = deterministic::Runner::timed(Duration::from_secs(60));
1002        runner.start(|mut context| async move {
1003            let Fixture {
1004                participants,
1005                schemes,
1006                ..
1007            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1008            let mut oracle = setup_network_with_participants(
1009                context.child("network"),
1010                NZUsize!(1),
1011                participants.clone(),
1012            )
1013            .await;
1014
1015            let me = participants[0].clone();
1016            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1017
1018            let setup = CodingHarness::setup_validator(
1019                context.child("validator").with_attribute("index", 0),
1020                &mut oracle,
1021                me.clone(),
1022                ConstantProvider::new(schemes[0].clone()),
1023            )
1024            .await;
1025            let marshal = setup.mailbox;
1026            let shards = setup.extra;
1027
1028            let genesis_ctx = CodingCtx {
1029                round: Round::zero(),
1030                leader: default_leader(),
1031                parent: (View::zero(), genesis_commitment()),
1032            };
1033            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
1034            let genesis_parent_commitment = genesis_coding_commitment::<Sha256, _>(&genesis);
1035
1036            let round = Round::new(Epoch::zero(), View::new(1));
1037            let block_ctx = CodingCtx {
1038                round,
1039                leader: me.clone(),
1040                parent: (View::zero(), genesis_parent_commitment),
1041            };
1042            let block = make_coding_block(block_ctx.clone(), genesis.digest(), Height::new(1), 100);
1043            let coded_block = CodedBlock::new(block, coding_config, &Sequential);
1044            let commitment = coded_block.commitment();
1045            shards.proposed(round, coded_block);
1046
1047            context.sleep(Duration::from_millis(10)).await;
1048
1049            let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1050            let cfg = MarshaledConfig {
1051                application: mock_app,
1052                marshal,
1053                shards,
1054                scheme_provider: ConstantProvider::new(schemes[0].clone()),
1055                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
1056                strategy: Sequential,
1057            };
1058            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1059
1060            let shard_validity = marshaled
1061                .verify(block_ctx, commitment)
1062                .await
1063                .await
1064                .expect("verify result missing");
1065            assert!(shard_validity, "shard validity should pass");
1066
1067            let certify_result = marshaled
1068                .certify(round, commitment)
1069                .await
1070                .await
1071                .expect("certify result missing");
1072            assert!(
1073                certify_result,
1074                "height-1 block should certify with genesis as parent"
1075            );
1076        });
1077    }
1078
1079    /// Finalizing a descendant must not height-prune the shard-engine buffer before
1080    /// `try_repair_gaps` has consumed buffer-only ancestors.
1081    ///
1082    /// Places parent (height 1) and descendant (height 2) in the shard engine's
1083    /// reconstructed-block cache via `proposed()`, then reports a finalization
1084    /// for the descendant only.
1085    #[test_traced("WARN")]
1086    fn test_coding_store_finalization_does_not_prune_buffer_before_repair() {
1087        let runner = deterministic::Runner::timed(Duration::from_secs(60));
1088        runner.start(|mut context| async move {
1089            let Fixture {
1090                participants,
1091                schemes,
1092                ..
1093            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1094            let mut oracle = setup_network_with_participants(
1095                context.child("network"),
1096                NZUsize!(1),
1097                participants.clone(),
1098            )
1099            .await;
1100
1101            let setup = CodingHarness::setup_validator(
1102                context.child("validator").with_attribute("index", 0),
1103                &mut oracle,
1104                participants[0].clone(),
1105                ConstantProvider::new(schemes[0].clone()),
1106            )
1107            .await;
1108            let mut handle = harness::ValidatorHandle::<CodingHarness> {
1109                mailbox: setup.mailbox,
1110                extra: setup.extra,
1111            };
1112
1113            // Build a 2-block chain: parent at height 1, descendant at height 2.
1114            let parent_block = CodingHarness::make_test_block(
1115                Sha256::hash(b""),
1116                CodingHarness::genesis_parent_commitment(NUM_VALIDATORS as u16),
1117                Height::new(1),
1118                1,
1119                NUM_VALIDATORS as u16,
1120            );
1121            let parent_digest = CodingHarness::digest(&parent_block);
1122            let parent_commitment = CodingHarness::commitment(&parent_block);
1123
1124            let descendant_block = CodingHarness::make_test_block(
1125                parent_digest,
1126                parent_commitment,
1127                Height::new(2),
1128                2,
1129                NUM_VALIDATORS as u16,
1130            );
1131            let descendant_commitment = CodingHarness::commitment(&descendant_block);
1132
1133            // Seed the shard engine's reconstructed-block cache with both blocks.
1134            CodingHarness::propose(
1135                &mut handle,
1136                Round::new(Epoch::new(0), View::new(1)),
1137                &parent_block,
1138            )
1139            .await;
1140            CodingHarness::propose(
1141                &mut handle,
1142                Round::new(Epoch::new(0), View::new(2)),
1143                &descendant_block,
1144            )
1145            .await;
1146
1147            // Report finalization for the descendant only. The parent has no
1148            // finalization certificate: it must be archived by walking the
1149            // parent link from the descendant and sourcing the block from the
1150            // shard-engine buffer.
1151            let descendant_proposal = Proposal {
1152                round: Round::new(Epoch::new(0), View::new(2)),
1153                parent: View::new(1),
1154                payload: descendant_commitment,
1155            };
1156            let descendant_finalization =
1157                CodingHarness::make_finalization(descendant_proposal, &schemes, QUORUM);
1158            CodingHarness::report_finalization(&mut handle.mailbox, descendant_finalization).await;
1159
1160            // Wait until the descendant is archived: that proves finalization processing
1161            // has completed, at which point the parent must already have been repaired
1162            // from the shard buffer.
1163            while handle.mailbox.get_block(Height::new(2)).await.is_none() {
1164                context.sleep(Duration::from_millis(10)).await;
1165            }
1166
1167            let parent = handle.mailbox.get_block(Height::new(1)).await;
1168            assert!(
1169                parent.is_some(),
1170                "parent must be archived from shard buffer before height-prune evicts it"
1171            );
1172        });
1173    }
1174
1175    #[test_traced("WARN")]
1176    fn test_coding_init_processed_height() {
1177        harness::init_processed_height::<CodingHarness>();
1178    }
1179
1180    #[test_traced("INFO")]
1181    fn test_coding_broadcast_caches_block() {
1182        harness::broadcast_caches_block::<CodingHarness>();
1183    }
1184
1185    /// Test that certifying a lower-view block after a higher-view block succeeds.
1186    ///
1187    /// This is a critical test for crash recovery scenarios where a validator may need
1188    /// to certify blocks in non-sequential view order.
1189    #[test_traced("INFO")]
1190    fn test_certify_lower_view_after_higher_view() {
1191        let runner = deterministic::Runner::timed(Duration::from_secs(60));
1192        runner.start(|mut context| async move {
1193            let Fixture {
1194                participants,
1195                schemes,
1196                ..
1197            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1198            let mut oracle = setup_network_with_participants(
1199                context.child("network"),
1200                NZUsize!(1),
1201                participants.clone(),
1202            )
1203            .await;
1204
1205            let me = participants[0].clone();
1206            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1207
1208            let setup = CodingHarness::setup_validator(
1209                context.child("validator").with_attribute("index", 0),
1210                &mut oracle,
1211                me.clone(),
1212                ConstantProvider::new(schemes[0].clone()),
1213            )
1214            .await;
1215            let marshal = setup.mailbox;
1216            let shards = setup.extra;
1217
1218            let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1219
1220            let cfg = MarshaledConfig {
1221                application: mock_app,
1222                marshal: marshal.clone(),
1223                shards: shards.clone(),
1224                scheme_provider: ConstantProvider::new(schemes[0].clone()),
1225                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
1226                strategy: Sequential,
1227            };
1228            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1229
1230            let genesis_ctx = CodingCtx {
1231                round: Round::zero(),
1232                leader: default_leader(),
1233                parent: (View::zero(), genesis_commitment()),
1234            };
1235            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
1236
1237            // Create parent block at height 1
1238            let parent_ctx = CodingCtx {
1239                round: Round::new(Epoch::new(0), View::new(1)),
1240                leader: default_leader(),
1241                parent: (View::zero(), genesis_commitment()),
1242            };
1243            let parent = make_coding_block(parent_ctx, genesis.digest(), Height::new(1), 100);
1244            let parent_digest = parent.digest();
1245            let coded_parent = CodedBlock::new(parent.clone(), coding_config, &Sequential);
1246            let parent_commitment = coded_parent.commitment();
1247            shards.proposed(Round::new(Epoch::new(0), View::new(1)), coded_parent);
1248
1249            // Block A at view 5 (height 2) - create with context matching what verify will receive
1250            let round_a = Round::new(Epoch::new(0), View::new(5));
1251            let context_a = CodingCtx {
1252                round: round_a,
1253                leader: me.clone(),
1254                parent: (View::new(1), parent_commitment),
1255            };
1256            let block_a = make_coding_block(context_a.clone(), parent_digest, Height::new(2), 200);
1257            let coded_block_a = CodedBlock::new(block_a.clone(), coding_config, &Sequential);
1258            let commitment_a = coded_block_a.commitment();
1259            shards.proposed(round_a, coded_block_a);
1260
1261            // Block B at view 10 (height 2, different block same height - could happen with
1262            // different proposers or re-proposals)
1263            let round_b = Round::new(Epoch::new(0), View::new(10));
1264            let context_b = CodingCtx {
1265                round: round_b,
1266                leader: me.clone(),
1267                parent: (View::new(1), parent_commitment),
1268            };
1269            let block_b = make_coding_block(context_b.clone(), parent_digest, Height::new(2), 300);
1270            let coded_block_b = CodedBlock::new(block_b.clone(), coding_config, &Sequential);
1271            let commitment_b = coded_block_b.commitment();
1272            shards.proposed(round_b, coded_block_b);
1273
1274            context.sleep(Duration::from_millis(10)).await;
1275
1276            // Step 1: Verify block A at view 5
1277            let _ = marshaled.verify(context_a, commitment_a).await.await;
1278
1279            // Step 2: Verify block B at view 10
1280            let _ = marshaled.verify(context_b, commitment_b).await.await;
1281
1282            // Step 3: Certify block B at view 10 FIRST
1283            let certify_b = marshaled.certify(round_b, commitment_b).await;
1284            assert!(
1285                certify_b.await.unwrap(),
1286                "Block B certification should succeed"
1287            );
1288
1289            // Step 4: Certify block A at view 5 - should succeed
1290            let certify_a = marshaled.certify(round_a, commitment_a).await;
1291
1292            // Use select with timeout to detect never-resolving receiver
1293            select! {
1294                result = certify_a => {
1295                    assert!(result.unwrap(), "Block A certification should succeed");
1296                },
1297                _ = context.sleep(Duration::from_secs(5)) => {
1298                    panic!("Block A certification timed out");
1299                },
1300            }
1301        })
1302    }
1303
1304    /// Regression test for re-proposal validation in optimistic_verify.
1305    ///
1306    /// Verifies that:
1307    /// 1. Valid re-proposals at epoch boundaries are accepted
1308    /// 2. Invalid re-proposals (not at epoch boundary) are rejected
1309    ///
1310    /// A re-proposal occurs when the parent digest equals the block being verified,
1311    /// meaning the same block is being proposed again in a new view.
1312    #[test_traced("INFO")]
1313    fn test_marshaled_reproposal_validation() {
1314        let runner = deterministic::Runner::timed(Duration::from_secs(60));
1315        runner.start(|mut context| async move {
1316            let Fixture {
1317                participants,
1318                schemes,
1319                ..
1320            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1321            let mut oracle = setup_network_with_participants(
1322                context.child("network"),
1323                NZUsize!(1),
1324                participants.clone(),
1325            )
1326            .await;
1327
1328            let me = participants[0].clone();
1329            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1330
1331            let setup = CodingHarness::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            let shards = setup.extra;
1340
1341            let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1342            let cfg = MarshaledConfig {
1343                application: mock_app,
1344                marshal: marshal.clone(),
1345                shards: shards.clone(),
1346                scheme_provider: ConstantProvider::new(schemes[0].clone()),
1347                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
1348                strategy: Sequential,
1349            };
1350            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1351
1352            let genesis_ctx = CodingCtx {
1353                round: Round::zero(),
1354                leader: default_leader(),
1355                parent: (View::zero(), genesis_commitment()),
1356            };
1357            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
1358
1359            // Build a chain up to the epoch boundary (height 19 is the last block in epoch 0
1360            // with BLOCKS_PER_EPOCH=20, since epoch 0 covers heights 0-19)
1361            let mut parent = genesis.digest();
1362            let mut last_view = View::zero();
1363            let mut last_commitment = genesis_commitment();
1364            for i in 1..BLOCKS_PER_EPOCH.get() {
1365                let round = Round::new(Epoch::new(0), View::new(i));
1366                let ctx = CodingCtx {
1367                    round,
1368                    leader: me.clone(),
1369                    parent: (last_view, last_commitment),
1370                };
1371                let block = make_coding_block(ctx.clone(), parent, Height::new(i), i * 100);
1372                let coded_block = CodedBlock::new(block.clone(), coding_config, &Sequential);
1373                last_commitment = coded_block.commitment();
1374                shards.proposed(round, coded_block);
1375                parent = block.digest();
1376                last_view = View::new(i);
1377            }
1378
1379            // Create the epoch boundary block (height 19, last block in epoch 0)
1380            let boundary_height = Height::new(BLOCKS_PER_EPOCH.get() - 1);
1381            let boundary_round = Round::new(Epoch::new(0), View::new(boundary_height.get()));
1382            let boundary_context = CodingCtx {
1383                round: boundary_round,
1384                leader: me.clone(),
1385                parent: (last_view, last_commitment),
1386            };
1387            let boundary_block = make_coding_block(
1388                boundary_context.clone(),
1389                parent,
1390                boundary_height,
1391                boundary_height.get() * 100,
1392            );
1393            let coded_boundary =
1394                CodedBlock::new(boundary_block.clone(), coding_config, &Sequential);
1395            let boundary_commitment = coded_boundary.commitment();
1396            shards.proposed(boundary_round, coded_boundary);
1397
1398            context.sleep(Duration::from_millis(10)).await;
1399
1400            // Test 1: Valid re-proposal at epoch boundary should be accepted
1401            // Re-proposal context: parent digest equals the block being verified
1402            // Re-proposals happen within the same epoch when the parent is the last block
1403            //
1404            // In the coding marshal, verify() returns shard validity while deferred_verify
1405            // runs in the background. We call verify() to register the certification gate task,
1406            // then certify() returns the deferred_verify result.
1407            let reproposal_round = Round::new(Epoch::new(0), View::new(20));
1408            let reproposal_context = CodingCtx {
1409                round: reproposal_round,
1410                leader: me.clone(),
1411                parent: (View::new(boundary_height.get()), boundary_commitment), // Parent IS the boundary block
1412            };
1413
1414            // Call verify to kick off deferred verification.
1415            // We must await the verify result to ensure the certification gate task is
1416            // registered before calling certify.
1417            let shard_validity = marshaled
1418                .verify(reproposal_context.clone(), boundary_commitment)
1419                .await
1420                .await;
1421            assert!(
1422                shard_validity.unwrap(),
1423                "Re-proposal verify should return true for shard validity"
1424            );
1425
1426            // Use certify to get the actual deferred_verify result
1427            let certify_result = marshaled
1428                .certify(reproposal_round, boundary_commitment)
1429                .await
1430                .await;
1431            assert!(
1432                certify_result.unwrap(),
1433                "Valid re-proposal at epoch boundary should be accepted"
1434            );
1435
1436            // Test 2: Invalid re-proposal (not at epoch boundary) should be rejected
1437            // Create a block at height 10 (not at epoch boundary)
1438            let non_boundary_height = Height::new(10);
1439            let non_boundary_round = Round::new(Epoch::new(0), View::new(10));
1440            // For simplicity, we'll create a fresh non-boundary block and test re-proposal
1441            let non_boundary_context = CodingCtx {
1442                round: non_boundary_round,
1443                leader: me.clone(),
1444                parent: (View::new(9), last_commitment), // Use a prior commitment
1445            };
1446            let non_boundary_block = make_coding_block(
1447                non_boundary_context.clone(),
1448                parent,
1449                non_boundary_height,
1450                1000,
1451            );
1452            let coded_non_boundary =
1453                CodedBlock::new(non_boundary_block.clone(), coding_config, &Sequential);
1454            let non_boundary_commitment = coded_non_boundary.commitment();
1455
1456            // Make the non-boundary block available
1457            shards.proposed(non_boundary_round, coded_non_boundary);
1458
1459            context.sleep(Duration::from_millis(10)).await;
1460
1461            // Attempt to re-propose the non-boundary block
1462            let invalid_reproposal_round = Round::new(Epoch::new(0), View::new(15));
1463            let invalid_reproposal_context = CodingCtx {
1464                round: invalid_reproposal_round,
1465                leader: me.clone(),
1466                parent: (View::new(10), non_boundary_commitment),
1467            };
1468
1469            // Call verify to kick off deferred verification.
1470            // We must await the verify result to ensure the certification gate task is
1471            // registered before calling certify.
1472            let shard_validity = marshaled
1473                .verify(invalid_reproposal_context, non_boundary_commitment)
1474                .await
1475                .await;
1476            assert!(
1477                !shard_validity.unwrap(),
1478                "Invalid re-proposal verify should return false"
1479            );
1480
1481            // Use certify to get the actual deferred_verify result
1482            let certify_result = marshaled
1483                .certify(invalid_reproposal_round, non_boundary_commitment)
1484                .await
1485                .await;
1486            assert!(
1487                !certify_result.unwrap(),
1488                "Invalid re-proposal (not at epoch boundary) should be rejected"
1489            );
1490
1491            // Test 3: Re-proposal with mismatched epoch should be rejected
1492            // This is a regression test - re-proposals must be in the same epoch as the block.
1493            let cross_epoch_reproposal_round = Round::new(Epoch::new(1), View::new(20));
1494            let cross_epoch_reproposal_context = CodingCtx {
1495                round: cross_epoch_reproposal_round,
1496                leader: me.clone(),
1497                parent: (View::new(boundary_height.get()), boundary_commitment),
1498            };
1499
1500            // Call verify to kick off deferred verification.
1501            // We must await the verify result to ensure the certification gate task is
1502            // registered before calling certify.
1503            let shard_validity = marshaled
1504                .verify(cross_epoch_reproposal_context.clone(), boundary_commitment)
1505                .await
1506                .await;
1507            assert!(
1508                !shard_validity.unwrap(),
1509                "Cross-epoch re-proposal verify should return false"
1510            );
1511
1512            // Use certify to get the actual deferred_verify result
1513            let certify_result = marshaled
1514                .certify(cross_epoch_reproposal_round, boundary_commitment)
1515                .await
1516                .await;
1517            assert!(
1518                !certify_result.unwrap(),
1519                "Re-proposal with mismatched epoch should be rejected"
1520            );
1521
1522            // Note: Tests for certify-only paths (crash recovery scenarios) are not included here
1523            // because they require multiple validators to reconstruct blocks from shards. In a
1524            // single-validator test setup, block reconstruction fails due to insufficient shards.
1525            // These paths are tested in integration tests with multiple validators.
1526        })
1527    }
1528
1529    #[test_traced("WARN")]
1530    fn test_marshaled_rejects_mismatched_context_digest() {
1531        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1532        runner.start(|mut context| async move {
1533            let Fixture {
1534                participants,
1535                schemes,
1536                ..
1537            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1538            let mut oracle = setup_network_with_participants(
1539                context.child("network"),
1540                NZUsize!(1),
1541                participants.clone(),
1542            )
1543            .await;
1544
1545            let me = participants[0].clone();
1546            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1547
1548            let setup = CodingHarness::setup_validator(
1549                context.child("validator").with_attribute("index", 0),
1550                &mut oracle,
1551                me.clone(),
1552                ConstantProvider::new(schemes[0].clone()),
1553            )
1554            .await;
1555            let marshal = setup.mailbox;
1556            let shards = setup.extra;
1557
1558            let genesis_ctx = CodingCtx {
1559                round: Round::zero(),
1560                leader: default_leader(),
1561                parent: (View::zero(), genesis_commitment()),
1562            };
1563            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
1564
1565            let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1566            let cfg = MarshaledConfig {
1567                application: mock_app,
1568                marshal: marshal.clone(),
1569                shards: shards.clone(),
1570                scheme_provider: ConstantProvider::new(schemes[0].clone()),
1571                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
1572                strategy: Sequential,
1573            };
1574            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1575
1576            // Create parent block at height 1 so the commitment is well-formed.
1577            let parent_ctx = CodingCtx {
1578                round: Round::new(Epoch::zero(), View::new(1)),
1579                leader: default_leader(),
1580                parent: (View::zero(), genesis_commitment()),
1581            };
1582            let parent = make_coding_block(parent_ctx, genesis.digest(), Height::new(1), 100);
1583            let coded_parent = CodedBlock::new(parent.clone(), coding_config, &Sequential);
1584            let parent_commitment = coded_parent.commitment();
1585            shards.proposed(Round::new(Epoch::zero(), View::new(1)), coded_parent);
1586
1587            // Build a block with context A (commitment hash uses this context).
1588            let round_a = Round::new(Epoch::zero(), View::new(2));
1589            let context_a = CodingCtx {
1590                round: round_a,
1591                leader: me.clone(),
1592                parent: (View::new(1), parent_commitment),
1593            };
1594            let block_a = make_coding_block(context_a, parent.digest(), Height::new(2), 200);
1595            let coded_block_a: CodedBlock<_, ReedSolomon<Sha256>, Sha256> =
1596                CodedBlock::new(block_a, coding_config, &Sequential);
1597            let commitment_a = coded_block_a.commitment();
1598
1599            // Verify using a different consensus context B (hash mismatch).
1600            let round_b = Round::new(Epoch::zero(), View::new(3));
1601            let context_b = CodingCtx {
1602                round: round_b,
1603                leader: participants[1].clone(),
1604                parent: (View::new(1), parent_commitment),
1605            };
1606
1607            let verify_rx = marshaled.verify(context_b, commitment_a).await;
1608            select! {
1609                result = verify_rx => {
1610                    assert!(
1611                        !result.unwrap(),
1612                        "mismatched context digest should be rejected"
1613                    );
1614                },
1615                _ = context.sleep(Duration::from_secs(5)) => {
1616                    panic!("verify should reject mismatched context digest promptly");
1617                },
1618            }
1619        })
1620    }
1621
1622    #[test_traced("WARN")]
1623    fn test_reproposal_certify_recovers_after_verify_receiver_drop() {
1624        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1625        runner.start(|mut context| async move {
1626            let Fixture {
1627                participants,
1628                schemes,
1629                ..
1630            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1631            let mut oracle = setup_network_with_participants(
1632                context.child("network"),
1633                NZUsize!(1),
1634                participants.clone(),
1635            )
1636            .await;
1637
1638            let me = participants[0].clone();
1639            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1640
1641            let setup = CodingHarness::setup_validator(
1642                context.child("validator").with_attribute("index", 0),
1643                &mut oracle,
1644                me.clone(),
1645                ConstantProvider::new(schemes[0].clone()),
1646            )
1647            .await;
1648            let marshal = setup.mailbox;
1649            let shards = setup.extra;
1650
1651            let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1652            let cfg = MarshaledConfig {
1653                application: mock_app,
1654                marshal: marshal.clone(),
1655                shards: shards.clone(),
1656                scheme_provider: ConstantProvider::new(schemes[0].clone()),
1657                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
1658                strategy: Sequential,
1659            };
1660            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1661
1662            let genesis_ctx = CodingCtx {
1663                round: Round::zero(),
1664                leader: default_leader(),
1665                parent: (View::zero(), genesis_commitment()),
1666            };
1667            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
1668
1669            // Build a valid boundary re-proposal, but keep it unavailable until
1670            // after the optimistic verify receiver has been dropped.
1671            let boundary_height = Height::new(BLOCKS_PER_EPOCH.get() - 1);
1672            let boundary_round = Round::new(Epoch::zero(), View::new(boundary_height.get()));
1673            let boundary_context = CodingCtx {
1674                round: boundary_round,
1675                leader: me.clone(),
1676                parent: (View::zero(), genesis_commitment()),
1677            };
1678            let boundary_block = make_coding_block(
1679                boundary_context,
1680                genesis.digest(),
1681                boundary_height,
1682                boundary_height.get() * 100,
1683            );
1684            let coded_boundary = CodedBlock::new(boundary_block, coding_config, &Sequential);
1685            let boundary_commitment = coded_boundary.commitment();
1686            let reproposal_round = Round::new(Epoch::zero(), View::new(boundary_height.get() + 1));
1687            let reproposal_context = CodingCtx {
1688                round: reproposal_round,
1689                leader: me,
1690                parent: (View::new(boundary_height.get()), boundary_commitment),
1691            };
1692
1693            // Start verify, then drop the receiver before the block is available.
1694            let verify_rx = marshaled
1695                .verify(reproposal_context, boundary_commitment)
1696                .await;
1697            drop(verify_rx);
1698            context.sleep(Duration::from_millis(10)).await;
1699
1700            shards.proposed(boundary_round, coded_boundary);
1701            context.sleep(Duration::from_millis(10)).await;
1702
1703            // Certify should not return the stale closed certification gate task; it
1704            // should recover through the embedded-context certification path.
1705            let certify_rx = marshaled
1706                .certify(reproposal_round, boundary_commitment)
1707                .await;
1708            select! {
1709                result = certify_rx => {
1710                    assert!(
1711                        result.expect("certify result missing"),
1712                        "certify should recover after verify receiver drop"
1713                    );
1714                },
1715                _ = context.sleep(Duration::from_secs(5)) => {
1716                    panic!("certify should recover after verify receiver drop");
1717                },
1718            }
1719        })
1720    }
1721
1722    #[test_traced("WARN")]
1723    fn test_reproposal_missing_block_does_not_synthesize_false() {
1724        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1725        runner.start(|mut context| async move {
1726            let Fixture {
1727                participants,
1728                schemes,
1729                ..
1730            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1731            let mut oracle = setup_network_with_participants(
1732                context.child("network"),
1733                NZUsize!(1),
1734                participants.clone(),
1735            )
1736            .await;
1737
1738            let me = participants[0].clone();
1739            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1740
1741            let setup = CodingHarness::setup_validator(
1742                context.child("validator").with_attribute("index", 0),
1743                &mut oracle,
1744                me.clone(),
1745                ConstantProvider::new(schemes[0].clone()),
1746            )
1747            .await;
1748            let marshal = setup.mailbox;
1749            let shards = setup.extra;
1750
1751            let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1752            let cfg = MarshaledConfig {
1753                application: mock_app,
1754                marshal: marshal.clone(),
1755                shards: shards.clone(),
1756                scheme_provider: ConstantProvider::new(schemes[0].clone()),
1757                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
1758                strategy: Sequential,
1759            };
1760            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1761
1762            // Re-proposal payload with valid coding config, but no block available.
1763            let missing_payload = Commitment::from((
1764                Sha256::hash(b"missing_block"),
1765                Sha256::hash(b"missing_root"),
1766                Sha256::hash(b"missing_context"),
1767                coding_config,
1768            ));
1769            let round = Round::new(Epoch::zero(), View::new(1));
1770            let reproposal_context = CodingCtx {
1771                round,
1772                leader: me,
1773                parent: (View::zero(), missing_payload),
1774            };
1775
1776            // Verify must not synthesize `false` when the block cannot be fetched.
1777            let verify_rx = marshaled.verify(reproposal_context, missing_payload).await;
1778
1779            // Ensure the certification gate task has registered its subscription, then
1780            // force cancellation by pruning the missing commitment.
1781            context.sleep(Duration::from_millis(100)).await;
1782            shards.prune(missing_payload);
1783
1784            select! {
1785                result = verify_rx => {
1786                    assert!(
1787                        result.is_err(),
1788                        "verify should resolve without explicit false when re-proposal block is unavailable"
1789                    );
1790                },
1791                _ = context.sleep(Duration::from_secs(5)) => {
1792                    panic!("verify should resolve promptly when re-proposal block is unavailable");
1793                },
1794            }
1795
1796            // Certify should not surface the closed certification gate task as the final result.
1797            // With no block available, it remains pending on the recovery path until the
1798            // certifier's caller times out or data arrives.
1799            let mut certify_rx = marshaled.certify(round, missing_payload).await;
1800            context.sleep(Duration::from_millis(100)).await;
1801            assert!(
1802                matches!(
1803                    certify_rx.try_recv(),
1804                    Err(commonware_utils::channel::oneshot::error::TryRecvError::Empty)
1805                ),
1806                "certify should remain pending without explicit false or stale cancellation"
1807            );
1808            drop(certify_rx);
1809        })
1810    }
1811
1812    #[test_traced("WARN")]
1813    fn test_core_subscription_closes_when_coding_buffer_prunes_missing_commitment() {
1814        let runner = deterministic::Runner::timed(Duration::from_secs(30));
1815        runner.start(|mut context| async move {
1816            let Fixture {
1817                participants,
1818                schemes,
1819                ..
1820            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1821            let mut oracle = setup_network_with_participants(
1822                context.child("network"),
1823                NZUsize!(1),
1824                participants.clone(),
1825            )
1826            .await;
1827
1828            let setup = CodingHarness::setup_validator(
1829                context.child("validator").with_attribute("index", 0),
1830                &mut oracle,
1831                participants[0].clone(),
1832                ConstantProvider::new(schemes[0].clone()),
1833            )
1834            .await;
1835            let marshal = setup.mailbox;
1836            let shards = setup.extra;
1837
1838            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1839            let missing_commitment = Commitment::from((
1840                Sha256::hash(b"missing_block"),
1841                Sha256::hash(b"missing_root"),
1842                Sha256::hash(b"missing_context"),
1843                coding_config,
1844            ));
1845            let round = Round::new(Epoch::zero(), View::new(1));
1846
1847            // Subscribe through the core actor. This internally subscribes to the
1848            // coding shard buffer and registers local waiters.
1849            let block_rx = marshal.subscribe_by_commitment(
1850                missing_commitment,
1851                core::CommitmentFallback::FetchByRound { round },
1852            );
1853
1854            // Allow core actor to register the underlying buffer subscription.
1855            context.sleep(Duration::from_millis(100)).await;
1856
1857            // Prune the missing commitment in the shard engine, which should cancel
1858            // the underlying buffer subscription.
1859            shards.prune(missing_commitment);
1860
1861            // The core actor must surface cancellation by closing the subscription,
1862            // not by panicking or leaving the waiter parked indefinitely.
1863            select! {
1864                result = block_rx => {
1865                    assert!(
1866                        result.is_err(),
1867                        "core subscription should close when coding buffer drops subscription"
1868                    );
1869                },
1870                _ = context.sleep(Duration::from_secs(5)) => {
1871                    panic!("core subscription should resolve promptly after coding prune");
1872                },
1873            }
1874        })
1875    }
1876
1877    #[test_traced("WARN")]
1878    fn test_marshaled_rejects_unsupported_epoch() {
1879        #[derive(Clone)]
1880        struct LimitedEpocher {
1881            inner: FixedEpocher,
1882            max_epoch: u64,
1883        }
1884
1885        impl Epocher for LimitedEpocher {
1886            fn containing(&self, height: Height) -> Option<crate::types::EpochInfo> {
1887                let bounds = self.inner.containing(height)?;
1888                if bounds.epoch().get() > self.max_epoch {
1889                    None
1890                } else {
1891                    Some(bounds)
1892                }
1893            }
1894
1895            fn first(&self, epoch: Epoch) -> Option<Height> {
1896                if epoch.get() > self.max_epoch {
1897                    None
1898                } else {
1899                    self.inner.first(epoch)
1900                }
1901            }
1902
1903            fn last(&self, epoch: Epoch) -> Option<Height> {
1904                if epoch.get() > self.max_epoch {
1905                    None
1906                } else {
1907                    self.inner.last(epoch)
1908                }
1909            }
1910        }
1911
1912        let runner = deterministic::Runner::timed(Duration::from_secs(60));
1913        runner.start(|mut context| async move {
1914            let Fixture {
1915                participants,
1916                schemes,
1917                ..
1918            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
1919            let mut oracle = setup_network_with_participants(
1920                context.child("network"),
1921                NZUsize!(1),
1922                participants.clone(),
1923            )
1924            .await;
1925
1926            let me = participants[0].clone();
1927            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
1928
1929            let setup = CodingHarness::setup_validator(
1930                context.child("validator").with_attribute("index", 0),
1931                &mut oracle,
1932                me.clone(),
1933                ConstantProvider::new(schemes[0].clone()),
1934            )
1935            .await;
1936            let marshal = setup.mailbox;
1937            let shards = setup.extra;
1938
1939            let genesis_ctx = CodingCtx {
1940                round: Round::zero(),
1941                leader: default_leader(),
1942                parent: (View::zero(), genesis_commitment()),
1943            };
1944            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
1945
1946            let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
1947            let limited_epocher = LimitedEpocher {
1948                inner: FixedEpocher::new(BLOCKS_PER_EPOCH),
1949                max_epoch: 0,
1950            };
1951            let cfg = MarshaledConfig {
1952                application: mock_app,
1953                marshal: marshal.clone(),
1954                shards: shards.clone(),
1955                scheme_provider: ConstantProvider::new(schemes[0].clone()),
1956                epocher: limited_epocher,
1957                strategy: Sequential,
1958            };
1959            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
1960
1961            // Create a parent block at height 19 (last block in epoch 0, which is supported)
1962            let parent_ctx = CodingCtx {
1963                round: Round::new(Epoch::zero(), View::new(19)),
1964                leader: default_leader(),
1965                parent: (View::zero(), genesis_commitment()),
1966            };
1967            let parent = make_coding_block(parent_ctx, genesis.digest(), Height::new(19), 1000);
1968            let parent_digest = parent.digest();
1969            let coded_parent = CodedBlock::new(parent.clone(), coding_config, &Sequential);
1970            let parent_commitment = coded_parent.commitment();
1971            shards.proposed(Round::new(Epoch::zero(), View::new(19)), coded_parent);
1972
1973            // Create a block at height 20 (first block in epoch 1, which is NOT supported)
1974            let block_ctx = CodingCtx {
1975                round: Round::new(Epoch::new(1), View::new(20)),
1976                leader: default_leader(),
1977                parent: (View::new(19), parent_commitment),
1978            };
1979            let block = make_coding_block(block_ctx, parent_digest, Height::new(20), 2000);
1980            let coded_block = CodedBlock::new(block.clone(), coding_config, &Sequential);
1981            let block_commitment = coded_block.commitment();
1982            shards.proposed(Round::new(Epoch::new(1), View::new(20)), coded_block);
1983
1984            context.sleep(Duration::from_millis(10)).await;
1985
1986            // In the coding marshal, verify() returns shard validity while deferred_verify
1987            // runs in the background. We need to use certify() to get the deferred_verify result.
1988            let unsupported_round = Round::new(Epoch::new(1), View::new(20));
1989            let unsupported_context = CodingCtx {
1990                round: unsupported_round,
1991                leader: me.clone(),
1992                parent: (View::new(19), parent_commitment),
1993            };
1994
1995            // Call verify to kick off deferred verification
1996            let _shard_validity = marshaled
1997                .verify(unsupported_context, block_commitment)
1998                .await;
1999
2000            // Use certify to get the actual deferred_verify result
2001            let certify_result = marshaled
2002                .certify(unsupported_round, block_commitment)
2003                .await
2004                .await;
2005
2006            assert!(
2007                !certify_result.unwrap(),
2008                "Block in unsupported epoch should be rejected"
2009            );
2010        })
2011    }
2012
2013    #[test_traced("WARN")]
2014    fn test_marshaled_rejects_invalid_ancestry() {
2015        let runner = deterministic::Runner::timed(Duration::from_secs(60));
2016        runner.start(|mut context| async move {
2017            let Fixture {
2018                participants,
2019                schemes,
2020                ..
2021            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2022            let mut oracle = setup_network_with_participants(
2023                context.child("network"),
2024                NZUsize!(1),
2025                participants.clone(),
2026            )
2027            .await;
2028
2029            let me = participants[0].clone();
2030            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2031
2032            let setup = CodingHarness::setup_validator(
2033                context.child("validator").with_attribute("index", 0),
2034                &mut oracle,
2035                me.clone(),
2036                ConstantProvider::new(schemes[0].clone()),
2037            )
2038            .await;
2039            let marshal = setup.mailbox;
2040            let shards = setup.extra;
2041
2042            // Create genesis block
2043            let genesis_ctx = CodingCtx {
2044                round: Round::zero(),
2045                leader: default_leader(),
2046                parent: (View::zero(), genesis_commitment()),
2047            };
2048            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2049
2050            // Wrap with Marshaled verifier
2051            let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
2052            let cfg = MarshaledConfig {
2053                application: mock_app,
2054                marshal: marshal.clone(),
2055                shards: shards.clone(),
2056                scheme_provider: ConstantProvider::new(schemes[0].clone()),
2057                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2058                strategy: Sequential,
2059            };
2060            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2061
2062            // Test case 1: Non-contiguous height
2063            //
2064            // We need both blocks in the same epoch.
2065            // With BLOCKS_PER_EPOCH=20: epoch 0 is heights 0-19, epoch 1 is heights 20-39
2066            //
2067            // Store honest parent at height 21 (epoch 1)
2068            let honest_parent_ctx = CodingCtx {
2069                round: Round::new(Epoch::new(1), View::new(21)),
2070                leader: default_leader(),
2071                parent: (View::zero(), genesis_commitment()),
2072            };
2073            let honest_parent = make_coding_block(
2074                honest_parent_ctx,
2075                genesis.digest(),
2076                Height::new(BLOCKS_PER_EPOCH.get() + 1),
2077                1000,
2078            );
2079            let parent_digest = honest_parent.digest();
2080            let coded_parent = CodedBlock::new(honest_parent.clone(), coding_config, &Sequential);
2081            let parent_commitment = coded_parent.commitment();
2082            shards.proposed(Round::new(Epoch::new(1), View::new(21)), coded_parent);
2083
2084            // Byzantine proposer broadcasts malicious block at height 35
2085            // The block has the correct context (matching what consensus will provide)
2086            // but contains invalid content (non-contiguous height: 21 -> 35 instead of 21 -> 22)
2087            let byzantine_round = Round::new(Epoch::new(1), View::new(35));
2088            let byzantine_context = CodingCtx {
2089                round: byzantine_round,
2090                leader: me.clone(),
2091                parent: (View::new(21), parent_commitment), // Consensus says parent is at height 21
2092            };
2093            let malicious_block = make_coding_block(
2094                byzantine_context.clone(),
2095                parent_digest,
2096                Height::new(BLOCKS_PER_EPOCH.get() + 15), // Byzantine: non-contiguous height
2097                2000,
2098            );
2099            let coded_malicious =
2100                CodedBlock::new(malicious_block.clone(), coding_config, &Sequential);
2101            let malicious_commitment = coded_malicious.commitment();
2102            shards.proposed(byzantine_round, coded_malicious);
2103
2104            // Small delay to ensure broadcast is processed
2105            context.sleep(Duration::from_millis(10)).await;
2106
2107            // Marshaled.verify() kicks off deferred verification in the background.
2108            // The Marshaled verifier will:
2109            // 1. Fetch honest_parent (height 21) from marshal based on context.parent
2110            // 2. Fetch malicious_block (height 35) from marshal based on digest
2111            // 3. Validate height is contiguous (fail)
2112            // 4. Return false
2113            let _shard_validity = marshaled
2114                .verify(byzantine_context, malicious_commitment)
2115                .await;
2116
2117            // Use certify to get the actual deferred_verify result
2118            let certify_result = marshaled
2119                .certify(byzantine_round, malicious_commitment)
2120                .await
2121                .await;
2122
2123            assert!(
2124                !certify_result.unwrap(),
2125                "Byzantine block with non-contiguous heights should be rejected"
2126            );
2127
2128            // Test case 2: Mismatched parent digest
2129            //
2130            // Create another malicious block with correct context and height
2131            // but referencing the wrong parent digest (genesis instead of honest_parent)
2132            let byzantine_round2 = Round::new(Epoch::new(1), View::new(22));
2133            let byzantine_context2 = CodingCtx {
2134                round: byzantine_round2,
2135                leader: me.clone(),
2136                parent: (View::new(21), parent_commitment), // Consensus says parent is at height 21
2137            };
2138            let malicious_block2 = make_coding_block(
2139                byzantine_context2.clone(),
2140                genesis.digest(), // Byzantine: wrong parent digest
2141                Height::new(BLOCKS_PER_EPOCH.get() + 2),
2142                3000,
2143            );
2144            let coded_malicious2 =
2145                CodedBlock::new(malicious_block2.clone(), coding_config, &Sequential);
2146            let malicious_commitment2 = coded_malicious2.commitment();
2147            shards.proposed(byzantine_round2, coded_malicious2);
2148
2149            // Small delay to ensure broadcast is processed
2150            context.sleep(Duration::from_millis(10)).await;
2151
2152            // Marshaled.verify() kicks off deferred verification in the background.
2153            // The Marshaled verifier will:
2154            // 1. Fetch honest_parent (height 21) from marshal based on context.parent
2155            // 2. Fetch malicious_block (height 22) from marshal based on digest
2156            // 3. Validate height is contiguous
2157            // 4. Validate parent commitment matches (fail)
2158            // 5. Return false
2159            let _shard_validity = marshaled
2160                .verify(byzantine_context2, malicious_commitment2)
2161                .await;
2162
2163            // Use certify to get the actual deferred_verify result
2164            let certify_result = marshaled
2165                .certify(byzantine_round2, malicious_commitment2)
2166                .await
2167                .await;
2168
2169            assert!(
2170                !certify_result.unwrap(),
2171                "Byzantine block with mismatched parent commitment should be rejected"
2172            );
2173        })
2174    }
2175
2176    #[test_traced("WARN")]
2177    fn test_certify_without_prior_verify_crash_recovery() {
2178        // After a crash, consensus may call certify() without a prior verify().
2179        // The certify path should:
2180        //   1. Find no in-progress certification gate task
2181        //   2. Subscribe to the block from the shard engine
2182        //   3. Use the block's embedded context for deferred_verify
2183        //   4. Return Ok(true) for a valid block
2184        let runner = deterministic::Runner::timed(Duration::from_secs(30));
2185        runner.start(|mut context| async move {
2186            let Fixture {
2187                participants,
2188                schemes,
2189                ..
2190            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2191            let mut oracle = setup_network_with_participants(
2192                context.child("network"),
2193                NZUsize!(1),
2194                participants.clone(),
2195            )
2196            .await;
2197
2198            let me = participants[0].clone();
2199            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2200
2201            let setup = CodingHarness::setup_validator(
2202                context.child("validator").with_attribute("index", 0),
2203                &mut oracle,
2204                me.clone(),
2205                ConstantProvider::new(schemes[0].clone()),
2206            )
2207            .await;
2208            let marshal = setup.mailbox;
2209            let shards = setup.extra;
2210
2211            let genesis_ctx = CodingCtx {
2212                round: Round::zero(),
2213                leader: default_leader(),
2214                parent: (View::zero(), genesis_commitment()),
2215            };
2216            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2217
2218            let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
2219            let cfg = MarshaledConfig {
2220                application: mock_app,
2221                marshal: marshal.clone(),
2222                shards: shards.clone(),
2223                scheme_provider: ConstantProvider::new(schemes[0].clone()),
2224                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2225                strategy: Sequential,
2226            };
2227            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2228
2229            // Create parent at height 1.
2230            let parent_round = Round::new(Epoch::zero(), View::new(1));
2231            let parent_ctx = CodingCtx {
2232                round: parent_round,
2233                leader: default_leader(),
2234                parent: (View::zero(), genesis_commitment()),
2235            };
2236            let parent = make_coding_block(parent_ctx, genesis.digest(), Height::new(1), 100);
2237            let coded_parent = CodedBlock::new(parent.clone(), coding_config, &Sequential);
2238            let parent_commitment = coded_parent.commitment();
2239            shards.proposed(parent_round, coded_parent);
2240
2241            // Create child at height 2.
2242            let child_round = Round::new(Epoch::zero(), View::new(2));
2243            let child_ctx = CodingCtx {
2244                round: child_round,
2245                leader: me.clone(),
2246                parent: (View::new(1), parent_commitment),
2247            };
2248            let child = make_coding_block(child_ctx, parent.digest(), Height::new(2), 200);
2249            let coded_child = CodedBlock::new(child, coding_config, &Sequential);
2250            let child_commitment = coded_child.commitment();
2251            shards.proposed(child_round, coded_child);
2252
2253            context.sleep(Duration::from_millis(10)).await;
2254
2255            // Call certify directly without any prior verify (simulating crash recovery).
2256            let certify_rx = marshaled.certify(child_round, child_commitment).await;
2257            select! {
2258                result = certify_rx => {
2259                    assert!(
2260                        result.unwrap(),
2261                        "certify without prior verify should succeed for valid block"
2262                    );
2263                },
2264                _ = context.sleep(Duration::from_secs(5)) => {
2265                    panic!("certify should complete within timeout");
2266                },
2267            }
2268        })
2269    }
2270
2271    /// Regression test: a Byzantine leader must not be able to crash honest nodes
2272    /// by proposing a `Commitment` with invalid `CodingConfig` bytes (e.g.
2273    /// zero-valued `NonZeroU16` fields). The fix validates the embedded config
2274    /// during deserialization so malformed commitments are rejected at the codec
2275    /// level before reaching `verify()`.
2276    #[test_traced("WARN")]
2277    fn test_malformed_commitment_config_rejected_at_deserialization() {
2278        use commonware_codec::{Encode, ReadExt};
2279
2280        // Construct a Commitment with all-zero bytes (invalid CodingConfig:
2281        // minimum_shards=0, extra_shards=0). Serialize it and attempt to
2282        // deserialize -- this must fail.
2283        let malformed_bytes = [0u8; Commitment::SIZE];
2284        let result = Commitment::read(&mut &malformed_bytes[..]);
2285        assert!(
2286            result.is_err(),
2287            "deserialization of Commitment with zeroed CodingConfig must fail"
2288        );
2289
2290        // A validly-constructed Commitment must still round-trip.
2291        let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2292        let valid = Commitment::from((
2293            Sha256::hash(b"block"),
2294            Sha256::hash(b"root"),
2295            Sha256::hash(b"context"),
2296            coding_config,
2297        ));
2298        let encoded = valid.encode();
2299        let decoded =
2300            Commitment::read(&mut &encoded[..]).expect("valid Commitment must deserialize");
2301        assert_eq!(valid, decoded);
2302    }
2303
2304    #[test_traced("WARN")]
2305    fn test_certify_propagates_application_verify_failure() {
2306        let runner = deterministic::Runner::timed(Duration::from_secs(30));
2307        runner.start(|mut context| async move {
2308            // 1) Set up a single validator marshal stack.
2309            let Fixture {
2310                participants,
2311                schemes,
2312                ..
2313            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2314            let mut oracle = setup_network_with_participants(
2315                context.child("network"),
2316                NZUsize!(1),
2317                participants.clone(),
2318            )
2319            .await;
2320
2321            let me = participants[0].clone();
2322            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2323
2324            let setup = CodingHarness::setup_validator(
2325                context.child("validator").with_attribute("index", 0),
2326                &mut oracle,
2327                me.clone(),
2328                ConstantProvider::new(schemes[0].clone()),
2329            )
2330            .await;
2331            let marshal = setup.mailbox;
2332            let shards = setup.extra;
2333
2334            let genesis_ctx = CodingCtx {
2335                round: Round::zero(),
2336                leader: default_leader(),
2337                parent: (View::zero(), genesis_commitment()),
2338            };
2339            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2340            // 2) Force application verification to fail in deferred verification.
2341            let mock_app: MockVerifyingApp<CodingB, S> =
2342                MockVerifyingApp::with_verify_result(false);
2343
2344            let cfg = MarshaledConfig {
2345                application: mock_app,
2346                marshal: marshal.clone(),
2347                shards: shards.clone(),
2348                scheme_provider: ConstantProvider::new(schemes[0].clone()),
2349                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2350                strategy: Sequential,
2351            };
2352            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2353
2354            let parent_round = Round::new(Epoch::zero(), View::new(1));
2355            let parent_context = CodingCtx {
2356                round: parent_round,
2357                leader: me.clone(),
2358                parent: (View::zero(), genesis_commitment()),
2359            };
2360            let parent = make_coding_block(parent_context, genesis.digest(), Height::new(1), 100);
2361            let coded_parent = CodedBlock::new(parent.clone(), coding_config, &Sequential);
2362            let parent_commitment = coded_parent.commitment();
2363            shards.proposed(parent_round, coded_parent);
2364
2365            // 3) Publish a valid child so optimistic verify can succeed.
2366            let round = Round::new(Epoch::zero(), View::new(2));
2367            let verify_context = CodingCtx {
2368                round,
2369                leader: me,
2370                parent: (View::new(1), parent_commitment),
2371            };
2372            let block =
2373                make_coding_block(verify_context.clone(), parent.digest(), Height::new(2), 200);
2374            let coded_block = CodedBlock::new(block, coding_config, &Sequential);
2375            let commitment = coded_block.commitment();
2376            shards.proposed(round, coded_block);
2377
2378            context.sleep(Duration::from_millis(10)).await;
2379
2380            let optimistic = marshaled.verify(verify_context, commitment).await;
2381            assert!(
2382                optimistic.await.expect("verify result missing"),
2383                "optimistic verify should pass pre-checks and schedule deferred verification"
2384            );
2385
2386            // 4) Certify must observe the deferred application failure and return false.
2387            let certify = marshaled.certify(round, commitment).await;
2388            assert!(
2389                !certify.await.expect("certify result missing"),
2390                "certify should propagate deferred application verify failure"
2391            );
2392        })
2393    }
2394
2395    #[test_traced("WARN")]
2396    fn test_backfill_block_mismatched_commitment() {
2397        // Regression: when backfilling by Key::Block(commitment), a peer may return
2398        // a coded block with matching inner digest but a different coding commitment.
2399        // If a finalization for this digest is already cached, marshal must reject
2400        // the block unless V::commitment(block) matches the finalization payload.
2401        let runner = deterministic::Runner::timed(Duration::from_secs(30));
2402        runner.start(|mut context| async move {
2403            let Fixture {
2404                participants,
2405                schemes,
2406                ..
2407            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2408            let mut oracle = setup_network_with_participants(
2409                context.child("network"),
2410                NZUsize!(1),
2411                participants[..2].iter().cloned(),
2412            )
2413            .await;
2414
2415            let coding_config_a = coding_config_for_participants(NUM_VALIDATORS as u16);
2416            // Same total shards (4) but different min/extra split produces a different
2417            // coding root and config bytes, yielding a different commitment.
2418            let coding_config_b = commonware_coding::Config {
2419                minimum_shards: coding_config_a.minimum_shards.checked_add(1).unwrap(),
2420                extra_shards: NZU16!(coding_config_a.extra_shards.get() - 1),
2421            };
2422
2423            let v0_setup = CodingHarness::setup_validator(
2424                context.child("validator").with_attribute("index", 0),
2425                &mut oracle,
2426                participants[0].clone(),
2427                ConstantProvider::new(schemes[0].clone()),
2428            )
2429            .await;
2430            let v1_setup = CodingHarness::setup_validator(
2431                context.child("validator").with_attribute("index", 1),
2432                &mut oracle,
2433                participants[1].clone(),
2434                ConstantProvider::new(schemes[1].clone()),
2435            )
2436            .await;
2437
2438            setup_network_links(&mut oracle, &participants[..2], LINK).await;
2439
2440            let mut v0_mailbox = v0_setup.mailbox;
2441            let v1_mailbox = v1_setup.mailbox;
2442
2443            let genesis_ctx = CodingCtx {
2444                round: Round::zero(),
2445                leader: default_leader(),
2446                parent: (View::zero(), genesis_commitment()),
2447            };
2448            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2449
2450            let round1 = Round::new(Epoch::zero(), View::new(1));
2451            let block1_ctx = CodingCtx {
2452                round: round1,
2453                leader: participants[0].clone(),
2454                parent: (View::zero(), genesis_commitment()),
2455            };
2456            let block1 = make_coding_block(block1_ctx, genesis.digest(), Height::new(1), 100);
2457
2458            let coded_block_a: CodedBlock<_, ReedSolomon<Sha256>, Sha256> =
2459                CodedBlock::new(block1.clone(), coding_config_a, &Sequential);
2460            let commitment_a = coded_block_a.commitment();
2461
2462            let coded_block_b: CodedBlock<_, ReedSolomon<Sha256>, Sha256> =
2463                CodedBlock::new(block1.clone(), coding_config_b, &Sequential);
2464            let commitment_b = coded_block_b.commitment();
2465
2466            assert_eq!(coded_block_a.digest(), coded_block_b.digest());
2467            assert_ne!(commitment_a, commitment_b);
2468
2469            // Validator 1 proposes coded_block_b (same inner block, different coding).
2470            // This stores it in v1's shard engine and actor cache.
2471            assert!(v1_mailbox.verified(round1, coded_block_b.clone()).await);
2472            context.sleep(Duration::from_millis(100)).await;
2473
2474            // Create finalization referencing commitment_a (the "correct" commitment).
2475            let proposal: Proposal<Commitment> = Proposal {
2476                round: round1,
2477                parent: View::zero(),
2478                payload: commitment_a,
2479            };
2480            let finalization = CodingHarness::make_finalization(proposal.clone(), &schemes, QUORUM);
2481
2482            // Report finalization to v0. v0 doesn't have the block:
2483            //   - it fetches Key::Block(commitment)
2484            //   - v1 responds with coded_block_b (same digest, wrong commitment)
2485            //   - deliver path must reject because the response commitment does not
2486            //     match the request key
2487            CodingHarness::report_finalization(&mut v0_mailbox, finalization).await;
2488
2489            // Wait for the fetch cycle to complete.
2490            context.sleep(Duration::from_secs(5)).await;
2491
2492            // The mismatched block must not be stored.
2493            let stored = v0_mailbox.get_block(Height::new(1)).await;
2494            assert!(
2495                stored.is_none(),
2496                "v0 should reject backfilled block with mismatched commitment"
2497            );
2498
2499            // Without the block, finalization should not be persisted by height yet.
2500            let stored_finalization = v0_mailbox.get_finalization(Height::new(1)).await;
2501            assert!(
2502                stored_finalization.is_none(),
2503                "finalization should not be archived until matching block is available"
2504            );
2505        })
2506    }
2507
2508    #[test_traced("WARN")]
2509    #[should_panic(expected = "floor block parent commitment mismatch")]
2510    fn test_coding_floor_anchor_panics_on_parent_commitment_mismatch() {
2511        let runner = deterministic::Runner::timed(Duration::from_secs(30));
2512        runner.start(|mut context| async move {
2513            let Fixture {
2514                participants,
2515                schemes,
2516                ..
2517            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2518            let (mailbox, resolver, _actor_handle) = start_coding_actor_with_recording(
2519                context.child("validator"),
2520                "floor-parent-commitment-mismatch",
2521                ConstantProvider::new(schemes[0].clone()),
2522                RecordingCodingBuffer::default(),
2523            )
2524            .await;
2525
2526            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2527            let parent_round = Round::new(Epoch::zero(), View::new(1));
2528            let parent_context = CodingCtx {
2529                round: parent_round,
2530                leader: participants[0].clone(),
2531                parent: (View::zero(), genesis_commitment()),
2532            };
2533            let parent = make_coding_block(parent_context, Sha256::hash(b""), Height::new(1), 100);
2534
2535            let floor_round = Round::new(Epoch::zero(), View::new(2));
2536            let bad_context = CodingCtx {
2537                round: floor_round,
2538                leader: participants[0].clone(),
2539                parent: (View::new(1), genesis_commitment()),
2540            };
2541            let floor_block = make_coding_block(bad_context, parent.digest(), Height::new(2), 200);
2542            let coded_floor = CodedBlock::new(floor_block, coding_config, &Sequential);
2543            assert_ne!(
2544                coded_floor.parent(),
2545                coded_floor.context().parent.1.block::<D>()
2546            );
2547
2548            let finalization = CodingHarness::make_finalization(
2549                Proposal::new(
2550                    floor_round,
2551                    View::new(1),
2552                    CodingHarness::commitment(&coded_floor),
2553                ),
2554                &schemes,
2555                QUORUM,
2556            );
2557            resolver.respond_to_next_fetch(coded_floor.encode());
2558            mailbox.set_floor(finalization);
2559            context.sleep(Duration::from_secs(5)).await;
2560        })
2561    }
2562
2563    /// When the scheme provider has no entry for the current epoch,
2564    /// `Marshaled::propose` and `Marshaled::verify` must return a dropped
2565    /// receiver (the consensus engine treats `RecvError` as "abstain").
2566    #[test_traced("WARN")]
2567    fn test_marshaled_missing_scheme_skips_propose_and_verify() {
2568        let runner = deterministic::Runner::timed(Duration::from_secs(30));
2569        runner.start(|mut context| async move {
2570            let Fixture {
2571                participants,
2572                schemes,
2573                ..
2574            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2575            let mut oracle = setup_network_with_participants(
2576                context.child("network"),
2577                NZUsize!(1),
2578                participants.clone(),
2579            )
2580            .await;
2581
2582            let me = participants[0].clone();
2583
2584            let setup = CodingHarness::setup_validator(
2585                context.child("validator").with_attribute("index", 0),
2586                &mut oracle,
2587                me.clone(),
2588                ConstantProvider::new(schemes[0].clone()),
2589            )
2590            .await;
2591
2592            let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
2593
2594            let cfg = MarshaledConfig {
2595                application: mock_app,
2596                marshal: setup.mailbox,
2597                shards: setup.extra,
2598                scheme_provider: EmptyProvider,
2599                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2600                strategy: Sequential,
2601            };
2602            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2603
2604            let ctx = CodingCtx {
2605                round: Round::new(Epoch::zero(), View::new(1)),
2606                leader: me.clone(),
2607                parent: (View::zero(), genesis_commitment()),
2608            };
2609
2610            // propose with a missing scheme returns a dropped sender
2611            let rx = marshaled.propose(ctx.clone()).await;
2612            assert!(rx.await.is_err());
2613
2614            // verify with a missing scheme returns a dropped sender
2615            let rx = marshaled.verify(ctx, genesis_commitment()).await;
2616            assert!(rx.await.is_err());
2617        });
2618    }
2619
2620    /// Regression: a validator must not vote finalize on a block that is not
2621    /// durably persisted. If `certify` resolves true, the block is on disk for
2622    /// this validator. We assert this by aborting the marshal actor the
2623    /// instant `certify` returns true, then restarting from the same partition.
2624    #[test_traced("WARN")]
2625    fn test_marshaled_certify_persists_block_before_resolving() {
2626        for seed in 0u64..16 {
2627            certify_persists_block_before_resolving_at(seed);
2628        }
2629    }
2630
2631    fn certify_persists_block_before_resolving_at(seed: u64) {
2632        let runner = deterministic::Runner::new(
2633            deterministic::Config::new()
2634                .with_seed(seed)
2635                .with_timeout(Some(Duration::from_secs(60))),
2636        );
2637        runner.start(|mut context| async move {
2638            let Fixture {
2639                participants,
2640                schemes,
2641                ..
2642            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2643            let mut oracle = setup_network_with_participants(
2644                context.child("network"),
2645                NZUsize!(1),
2646                participants.clone(),
2647            )
2648            .await;
2649
2650            let me = participants[0].clone();
2651            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2652
2653            let setup = CodingHarness::setup_validator(
2654                context.child("validator").with_attribute("index", 0),
2655                &mut oracle,
2656                me.clone(),
2657                ConstantProvider::new(schemes[0].clone()),
2658            )
2659            .await;
2660            let marshal = setup.mailbox;
2661            let shards = setup.extra;
2662            let marshal_actor_handle = setup.actor_handle;
2663
2664            let genesis_ctx = CodingCtx {
2665                round: Round::zero(),
2666                leader: default_leader(),
2667                parent: (View::zero(), genesis_commitment()),
2668            };
2669            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2670
2671            // Push parent (height 1) and child (height 2) into the shards
2672            // engine. These are reconstructable but NOT durably persisted.
2673            let parent_round = Round::new(Epoch::zero(), View::new(1));
2674            let parent_ctx = CodingCtx {
2675                round: parent_round,
2676                leader: default_leader(),
2677                parent: (View::zero(), genesis_commitment()),
2678            };
2679            let parent = make_coding_block(parent_ctx, genesis.digest(), Height::new(1), 100);
2680            let coded_parent = CodedBlock::new(parent.clone(), coding_config, &Sequential);
2681            let parent_commitment = coded_parent.commitment();
2682            shards.proposed(parent_round, coded_parent);
2683
2684            let child_round = Round::new(Epoch::zero(), View::new(2));
2685            let child_ctx = CodingCtx {
2686                round: child_round,
2687                leader: me.clone(),
2688                parent: (View::new(1), parent_commitment),
2689            };
2690            let child = make_coding_block(child_ctx.clone(), parent.digest(), Height::new(2), 200);
2691            let coded_child = CodedBlock::new(child.clone(), coding_config, &Sequential);
2692            let child_commitment = coded_child.commitment();
2693            let child_digest = coded_child.digest();
2694            shards.proposed(child_round, coded_child);
2695
2696            context.sleep(Duration::from_millis(10)).await;
2697
2698            let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
2699            let cfg = MarshaledConfig {
2700                application: mock_app,
2701                marshal: marshal.clone(),
2702                shards: shards.clone(),
2703                scheme_provider: ConstantProvider::new(schemes[0].clone()),
2704                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2705                strategy: Sequential,
2706            };
2707            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2708
2709            // Optimistic verify - returns shard validity (true).
2710            let shard_validity = marshaled
2711                .verify(child_ctx, child_commitment)
2712                .await
2713                .await
2714                .expect("verify result missing");
2715            assert!(shard_validity, "shard validity should pass");
2716
2717            // Certify - this is the safety gate before finalize voting.
2718            let certify_result = marshaled
2719                .certify(child_round, child_commitment)
2720                .await
2721                .await
2722                .expect("certify result missing");
2723            assert!(certify_result, "certify should succeed");
2724
2725            // Abort marshal immediately after certify returns to prove the
2726            // block is already persisted at that point.
2727            marshal_actor_handle.abort();
2728            drop(marshaled);
2729            drop(marshal);
2730            drop(shards);
2731
2732            // Restart from the same partition. The block must be durably
2733            // persisted - otherwise the validator would have voted finalize
2734            // for a block it cannot serve from local storage.
2735            let setup2 = CodingHarness::setup_validator(
2736                context
2737                    .child("validator_restart")
2738                    .with_attribute("index", 0),
2739                &mut oracle,
2740                me.clone(),
2741                ConstantProvider::new(schemes[0].clone()),
2742            )
2743            .await;
2744            let marshal2 = setup2.mailbox;
2745
2746            let post_restart = marshal2.get_block(&child_digest).await;
2747            assert!(
2748                post_restart.is_some(),
2749                "certify resolved true, so block must be durably persisted"
2750            );
2751        });
2752    }
2753
2754    /// Regression: a leader must be able to recover its own block across an unclean restart.
2755    /// `propose` registers a certification gate, so the leader establishes durability by
2756    /// certifying its own proposal. After certify, the block must survive restart even if
2757    /// `Relay::broadcast` never runs. This is the >= f+1 guarantee for the leader's own block.
2758    #[test_traced("WARN")]
2759    fn test_marshaled_proposed_block_persists_across_restart() {
2760        let runner = deterministic::Runner::timed(Duration::from_secs(60));
2761        runner.start(|mut context| async move {
2762            let Fixture {
2763                participants,
2764                schemes,
2765                ..
2766            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2767            let mut oracle = setup_network_with_participants(
2768                context.child("network"),
2769                NZUsize!(1),
2770                participants.clone(),
2771            )
2772            .await;
2773
2774            let me = participants[0].clone();
2775            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2776
2777            let setup = CodingHarness::setup_validator(
2778                context.child("validator").with_attribute("index", 0),
2779                &mut oracle,
2780                me.clone(),
2781                ConstantProvider::new(schemes[0].clone()),
2782            )
2783            .await;
2784            let marshal = setup.mailbox;
2785            let shards = setup.extra;
2786            let marshal_actor_handle = setup.actor_handle;
2787
2788            let genesis_ctx = CodingCtx {
2789                round: Round::zero(),
2790                leader: default_leader(),
2791                parent: (View::zero(), genesis_commitment()),
2792            };
2793            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2794            let genesis_parent_commitment = genesis_coding_commitment::<Sha256, _>(&genesis);
2795
2796            // Build the block we want propose() to return. Its embedded context
2797            // uses the proper genesis commitment so the parent lookup matches
2798            // the cached genesis.
2799            let propose_round = Round::new(Epoch::zero(), View::new(1));
2800            let propose_context = CodingCtx {
2801                round: propose_round,
2802                leader: me.clone(),
2803                parent: (View::zero(), genesis_parent_commitment),
2804            };
2805            let block_to_propose = make_coding_block(
2806                propose_context.clone(),
2807                genesis.digest(),
2808                Height::new(1),
2809                100,
2810            );
2811            let block_digest = block_to_propose.digest();
2812            let expected_commitment = CodedBlock::<_, ReedSolomon<Sha256>, Sha256>::new(
2813                block_to_propose.clone(),
2814                coding_config,
2815                &Sequential,
2816            )
2817            .commitment();
2818
2819            let mock_app: MockVerifyingApp<CodingB, S> =
2820                MockVerifyingApp::new().with_propose_result(block_to_propose);
2821            let cfg = MarshaledConfig {
2822                application: mock_app,
2823                marshal: marshal.clone(),
2824                shards: shards.clone(),
2825                scheme_provider: ConstantProvider::new(schemes[0].clone()),
2826                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2827                strategy: Sequential,
2828            };
2829            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2830
2831            // Drive the leader-side propose path. `propose` stages the block
2832            // and returns the commitment. Durability is established by the
2833            // certify flush below.
2834            let commitment = marshaled
2835                .propose(propose_context)
2836                .await
2837                .await
2838                .expect("propose should produce a commitment");
2839            assert_eq!(commitment, expected_commitment);
2840
2841            // The leader certifies its own proposal, which awaits the deferred propose
2842            // sync handle and establishes durability before the finalize vote.
2843            assert!(
2844                marshaled
2845                    .certify(propose_round, commitment)
2846                    .await
2847                    .await
2848                    .expect("certify result missing"),
2849                "certify must succeed for the leader's own proposal"
2850            );
2851
2852            // Abort marshal after certify; the leader's own block must be durable.
2853            marshal_actor_handle.abort();
2854            drop(marshaled);
2855            drop(marshal);
2856            drop(shards);
2857
2858            let setup2 = CodingHarness::setup_validator(
2859                context
2860                    .child("validator_restart")
2861                    .with_attribute("index", 0),
2862                &mut oracle,
2863                me.clone(),
2864                ConstantProvider::new(schemes[0].clone()),
2865            )
2866            .await;
2867            let marshal2 = setup2.mailbox;
2868
2869            // The proposer must recover its own block after restart. Without
2870            // the broadcast-path persistence fix, the block lived only in the
2871            // shards engine's in-memory cache and is now gone.
2872            let post_restart = marshal2.get_block(&block_digest).await;
2873            assert!(
2874                post_restart.is_some(),
2875                "proposer should recover its own block after restart"
2876            );
2877        });
2878    }
2879
2880    /// A propose relay with a staged proposal must send it through the shard
2881    /// engine and complete the durability handshake. The freshly built block
2882    /// is nowhere persisted at broadcast time, so the forward fallback has
2883    /// nothing to serve: only the staged-hit path can seed the shard engine.
2884    #[test_traced("WARN")]
2885    fn test_marshaled_propose_relay_sends_staged_block() {
2886        let runner = deterministic::Runner::timed(Duration::from_secs(60));
2887        runner.start(|mut context| async move {
2888            let Fixture {
2889                participants,
2890                schemes,
2891                ..
2892            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
2893            let mut oracle = setup_network_with_participants(
2894                context.child("network"),
2895                NZUsize!(1),
2896                participants.clone(),
2897            )
2898            .await;
2899
2900            let me = participants[0].clone();
2901            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
2902
2903            let setup = CodingHarness::setup_validator(
2904                context.child("validator").with_attribute("index", 0),
2905                &mut oracle,
2906                me.clone(),
2907                ConstantProvider::new(schemes[0].clone()),
2908            )
2909            .await;
2910            let marshal = setup.mailbox;
2911            let shards = setup.extra;
2912
2913            let genesis_ctx = CodingCtx {
2914                round: Round::zero(),
2915                leader: default_leader(),
2916                parent: (View::zero(), genesis_commitment()),
2917            };
2918            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
2919            let genesis_parent_commitment = genesis_coding_commitment::<Sha256, _>(&genesis);
2920
2921            let propose_round = Round::new(Epoch::zero(), View::new(1));
2922            let propose_context = CodingCtx {
2923                round: propose_round,
2924                leader: me.clone(),
2925                parent: (View::zero(), genesis_parent_commitment),
2926            };
2927            let block_to_propose = make_coding_block(
2928                propose_context.clone(),
2929                genesis.digest(),
2930                Height::new(1),
2931                100,
2932            );
2933            let block_digest = block_to_propose.digest();
2934            let expected_commitment = CodedBlock::<_, ReedSolomon<Sha256>, Sha256>::new(
2935                block_to_propose.clone(),
2936                coding_config,
2937                &Sequential,
2938            )
2939            .commitment();
2940
2941            let mock_app: MockVerifyingApp<CodingB, S> =
2942                MockVerifyingApp::new().with_propose_result(block_to_propose);
2943            let cfg = MarshaledConfig {
2944                application: mock_app,
2945                marshal: marshal.clone(),
2946                shards: shards.clone(),
2947                scheme_provider: ConstantProvider::new(schemes[0].clone()),
2948                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
2949                strategy: Sequential,
2950            };
2951            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
2952
2953            let commitment = marshaled
2954                .propose(propose_context)
2955                .await
2956                .await
2957                .expect("propose should produce a commitment");
2958            assert_eq!(commitment, expected_commitment);
2959
2960            // The relay must take the staged proposal and broadcast its
2961            // shards, seeding the shard engine's local cache.
2962            let subscription = shards.subscribe(commitment);
2963            let _ = marshaled.broadcast(
2964                commitment,
2965                Plan::Propose {
2966                    round: propose_round,
2967                },
2968            );
2969            let cached = subscription
2970                .await
2971                .expect("shard engine must cache the relayed proposal");
2972            assert_eq!(cached.digest(), block_digest);
2973
2974            // The relayed proposal is persisted through the staged ack, so
2975            // certification resolves durably without a flush.
2976            assert!(
2977                marshaled
2978                    .certify(propose_round, commitment)
2979                    .await
2980                    .await
2981                    .expect("certify result missing"),
2982                "certify must succeed for the relayed proposal"
2983            );
2984        });
2985    }
2986
2987    /// Regression: if marshal already holds a verified block for a round
2988    /// (say, persisted by a pre-crash propose whose notarize vote never
2989    /// reached the journal), a restarted leader's `propose` must return
2990    /// that block's commitment instead of rebuilding. The pre-crash
2991    /// commitment may already have been broadcast, so proposing a rebuilt
2992    /// block for the same round would equivocate. The recovered proposal
2993    /// must also be staged for the relay, so the broadcast re-sends its
2994    /// shards and certification resolves through the deduplicated
2995    /// re-persist.
2996    #[test_traced("WARN")]
2997    fn test_propose_reuses_verified_block_on_restart() {
2998        let runner = deterministic::Runner::timed(Duration::from_secs(60));
2999        runner.start(|mut context| async move {
3000            let Fixture {
3001                participants,
3002                schemes,
3003                ..
3004            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
3005            let mut oracle = setup_network_with_participants(
3006                context.child("network"),
3007                NZUsize!(1),
3008                participants.clone(),
3009            )
3010            .await;
3011
3012            let me = participants[0].clone();
3013            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
3014
3015            let setup = CodingHarness::setup_validator(
3016                context.child("validator").with_attribute("index", 0),
3017                &mut oracle,
3018                me.clone(),
3019                ConstantProvider::new(schemes[0].clone()),
3020            )
3021            .await;
3022            let marshal = setup.mailbox;
3023            let shards = setup.extra;
3024
3025            let genesis_ctx = CodingCtx {
3026                round: Round::zero(),
3027                leader: default_leader(),
3028                parent: (View::zero(), genesis_commitment()),
3029            };
3030            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
3031            let genesis_parent_commitment = genesis_coding_commitment::<Sha256, _>(&genesis);
3032
3033            let round = Round::new(Epoch::zero(), View::new(1));
3034            let ctx = CodingCtx {
3035                round,
3036                leader: me.clone(),
3037                parent: (View::zero(), genesis_parent_commitment),
3038            };
3039
3040            // Seed block A in marshal's verified cache for `round`.
3041            let block_a = make_coding_block(ctx.clone(), genesis.digest(), Height::new(1), 100);
3042            let coded_a: CodedBlock<_, ReedSolomon<Sha256>, Sha256> =
3043                CodedBlock::new(block_a.clone(), coding_config, &Sequential);
3044            let commitment_a = coded_a.commitment();
3045            assert!(marshal.verified(round, coded_a).await);
3046
3047            // The app cannot build (`propose` returns None) and its
3048            // verification never completes, so the assertions below hold
3049            // only if the stored block is reused as-is and certification
3050            // resolves through the durability gate registered by the
3051            // recovery staging.
3052            let (mock_app, verify_started, _release_verify): (GatedVerifyingApp<CodingB, S>, _, _) =
3053                GatedVerifyingApp::new();
3054            let cfg = MarshaledConfig {
3055                application: mock_app,
3056                marshal: marshal.clone(),
3057                shards: shards.clone(),
3058                scheme_provider: ConstantProvider::new(schemes[0].clone()),
3059                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
3060                strategy: Sequential,
3061            };
3062            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
3063
3064            let commitment = marshaled
3065                .propose(ctx)
3066                .await
3067                .await
3068                .expect("propose must return a commitment");
3069            assert_eq!(
3070                commitment, commitment_a,
3071                "propose must reuse the block marshal already persisted for this round"
3072            );
3073
3074            // The relay broadcast must find the recovered proposal staged and
3075            // re-persist it (a dedup no-op whose handle covers the pre-crash
3076            // write), resolving the certification gate registered by the
3077            // recovery path.
3078            let _ = marshaled.broadcast(commitment, Plan::Propose { round });
3079            let certify_rx = marshaled.certify(round, commitment).await;
3080            select! {
3081                result = certify_rx => {
3082                    assert!(
3083                        result.expect("certify result missing"),
3084                        "recovered proposal must certify through the relay handshake"
3085                    );
3086                },
3087                _ = verify_started => {
3088                    panic!("certifying a recovered proposal must not run app verification");
3089                },
3090            }
3091        });
3092    }
3093
3094    /// Regression: if a pre-crash leader persisted a verified block for a
3095    /// round but the simplex `Notarize` never reached the journal, replay
3096    /// can recover a `consensus_context` whose parent differs from the one
3097    /// the cached block was built against. The restarted leader must then
3098    /// drop the receiver so the voter nullifies the view via
3099    /// `MissingProposal`, rather than broadcasting the stale cached block
3100    /// under a header that peers will reject.
3101    #[test_traced("WARN")]
3102    fn test_propose_skips_when_verified_block_context_changed() {
3103        let runner = deterministic::Runner::timed(Duration::from_secs(60));
3104        runner.start(|mut context| async move {
3105            let Fixture {
3106                participants,
3107                schemes,
3108                ..
3109            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, NAMESPACE, NUM_VALIDATORS);
3110            let mut oracle = setup_network_with_participants(
3111                context.child("network"),
3112                NZUsize!(1),
3113                participants.clone(),
3114            )
3115            .await;
3116
3117            let me = participants[0].clone();
3118            let coding_config = coding_config_for_participants(NUM_VALIDATORS as u16);
3119
3120            let setup = CodingHarness::setup_validator(
3121                context.child("validator").with_attribute("index", 0),
3122                &mut oracle,
3123                me.clone(),
3124                ConstantProvider::new(schemes[0].clone()),
3125            )
3126            .await;
3127            let marshal = setup.mailbox;
3128            let shards = setup.extra;
3129
3130            let genesis_ctx = CodingCtx {
3131                round: Round::zero(),
3132                leader: default_leader(),
3133                parent: (View::zero(), genesis_commitment()),
3134            };
3135            let genesis = make_coding_block(genesis_ctx, Sha256::hash(b""), Height::zero(), 0);
3136            let genesis_parent_commitment = genesis_coding_commitment::<Sha256, _>(&genesis);
3137
3138            // Stash a stale block built against genesis as its parent at round V=2.
3139            let round = Round::new(Epoch::zero(), View::new(2));
3140            let stale_ctx = CodingCtx {
3141                round,
3142                leader: me.clone(),
3143                parent: (View::zero(), genesis_parent_commitment),
3144            };
3145            let stale_block = make_coding_block(stale_ctx, genesis.digest(), Height::new(1), 100);
3146            let stale_coded: CodedBlock<_, ReedSolomon<Sha256>, Sha256> =
3147                CodedBlock::new(stale_block, coding_config, &Sequential);
3148            assert!(marshal.verified(round, stale_coded).await);
3149
3150            // Simulate a replay where parent selection now points to a
3151            // different parent commitment than the cached block was built for.
3152            let new_parent_commitment = Commitment::from((
3153                Sha256::hash(b"different-parent-block"),
3154                Sha256::hash(b"different-parent-inner"),
3155                Sha256::hash(b"different-parent-ctx"),
3156                coding_config,
3157            ));
3158            let new_ctx = CodingCtx {
3159                round,
3160                leader: me.clone(),
3161                parent: (View::new(1), new_parent_commitment),
3162            };
3163
3164            let mock_app: MockVerifyingApp<CodingB, S> = MockVerifyingApp::new();
3165            let cfg = MarshaledConfig {
3166                application: mock_app,
3167                marshal: marshal.clone(),
3168                shards: shards.clone(),
3169                scheme_provider: ConstantProvider::new(schemes[0].clone()),
3170                epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
3171                strategy: Sequential,
3172            };
3173            let mut marshaled = Marshaled::new(context.child("marshaled"), cfg);
3174
3175            let commitment_rx = marshaled.propose(new_ctx).await;
3176            assert!(
3177                commitment_rx.await.is_err(),
3178                "propose must drop the receiver when the cached block's context no longer matches"
3179            );
3180        });
3181    }
3182}