@@ -934,6 +934,108 @@ pub fn verified_success_implies_recoverable_after_restart<H: TestHarness>(
934934 }
935935}
936936
937+ /// Regression: when the same block is verified at an earlier view and later
938+ /// certified at a much later view (epoch-boundary reproposal), both writes
939+ /// must land so retention can prune the earlier view without losing the
940+ /// block. A naive "skip the sibling write if the block's digest is already
941+ /// present in the other archive" optimization is unsafe because the two
942+ /// archives prune per-view on the same boundary: if the block lives only in
943+ /// `verified_blocks[V_early]` and never gets written to
944+ /// `notarized_blocks[V_late]`, advancing retention past V_early drops the
945+ /// block even though V_late is still within the window.
946+ pub fn certify_at_later_view_survives_earlier_view_pruning < H : TestHarness > ( ) {
947+ let runner = deterministic:: Runner :: timed ( Duration :: from_secs ( 60 ) ) ;
948+ runner. start ( |mut context| async move {
949+ let Fixture {
950+ participants,
951+ schemes,
952+ ..
953+ } = bls12381_threshold_vrf:: fixture :: < V , _ > ( & mut context, NAMESPACE , NUM_VALIDATORS ) ;
954+ let mut oracle =
955+ setup_network_with_participants ( context. clone ( ) , NZUsize ! ( 1 ) , participants. clone ( ) )
956+ . await ;
957+ let setup = H :: setup_validator (
958+ context. with_label ( "validator_0" ) ,
959+ & mut oracle,
960+ participants[ 0 ] . clone ( ) ,
961+ ConstantProvider :: new ( schemes[ 0 ] . clone ( ) ) ,
962+ )
963+ . await ;
964+ let application = setup. application ;
965+ let mut handle = ValidatorHandle :: < H > {
966+ mailbox : setup. mailbox ,
967+ extra : setup. extra ,
968+ } ;
969+
970+ // An off-chain block that we will verify at an early view and certify
971+ // at a later view. Its height is intentionally well beyond the chain
972+ // we'll drive below, so it never enters the finalized archive via
973+ // gap repair and lives solely in the prunable caches.
974+ let off_chain = H :: make_test_block (
975+ Sha256 :: hash ( b"" ) ,
976+ H :: genesis_parent_commitment ( NUM_VALIDATORS as u16 ) ,
977+ Height :: new ( 5_000 ) ,
978+ 9_999 ,
979+ NUM_VALIDATORS as u16 ,
980+ ) ;
981+ let off_chain_digest = H :: digest ( & off_chain) ;
982+
983+ // Verify at V=1, then certify at V=25 (reproposal-style gap).
984+ let v_early = Round :: new ( Epoch :: zero ( ) , View :: new ( 1 ) ) ;
985+ let v_late = Round :: new ( Epoch :: zero ( ) , View :: new ( 25 ) ) ;
986+ let mut peers: [ ValidatorHandle < H > ; 0 ] = [ ] ;
987+ H :: verify ( & mut handle, v_early, & off_chain, & mut peers) . await ;
988+ assert ! (
989+ H :: certify( & mut handle, v_late, & off_chain) . await ,
990+ "certify must ack"
991+ ) ;
992+
993+ // Drive the finalized chain forward to advance `last_processed_round`
994+ // past V=1's retention boundary but not past V=25's. With
995+ // view_retention_timeout=10 and prunable_items_per_section=10,
996+ // processing views 1..=21 leaves `oldest_allowed=10` in both prunable
997+ // archives — V=1 is dropped, V=25 is retained.
998+ const CHAIN_LEN : u64 = 21 ;
999+ let mut parent = Sha256 :: hash ( b"" ) ;
1000+ let mut parent_commitment = H :: genesis_parent_commitment ( NUM_VALIDATORS as u16 ) ;
1001+ for i in 1 ..=CHAIN_LEN {
1002+ let block = H :: make_test_block (
1003+ parent,
1004+ parent_commitment,
1005+ Height :: new ( i) ,
1006+ i,
1007+ NUM_VALIDATORS as u16 ,
1008+ ) ;
1009+ let digest = H :: digest ( & block) ;
1010+ let commitment = H :: commitment ( & block) ;
1011+ let round = Round :: new ( Epoch :: zero ( ) , View :: new ( i) ) ;
1012+ H :: propose ( & mut handle, round, & block) . await ;
1013+ let proposal = Proposal {
1014+ round,
1015+ parent : View :: new ( i - 1 ) ,
1016+ payload : commitment,
1017+ } ;
1018+ let finalization = H :: make_finalization ( proposal, & schemes, QUORUM ) ;
1019+ H :: report_finalization ( & mut handle. mailbox , finalization) . await ;
1020+ parent = digest;
1021+ parent_commitment = commitment;
1022+ }
1023+ while ( application. blocks ( ) . len ( ) as u64 ) < CHAIN_LEN {
1024+ context. sleep ( Duration :: from_millis ( 10 ) ) . await ;
1025+ }
1026+ context. sleep ( Duration :: from_millis ( 100 ) ) . await ;
1027+
1028+ // The off-chain block must still be retrievable: verified_blocks[V=1]
1029+ // has been pruned, but notarized_blocks[V=25] still holds it.
1030+ let recovered = handle. mailbox . get_block ( & off_chain_digest) . await ;
1031+ assert ! (
1032+ recovered. is_some( ) ,
1033+ "block certified at V=25 must survive retention pruning of V=1"
1034+ ) ;
1035+ assert_eq ! ( recovered. unwrap( ) . digest( ) , off_chain_digest) ;
1036+ } ) ;
1037+ }
1038+
9371039/// Regression: when a leader equivocates, a validator may verify one block
9381040/// (A) and then certify a different block (B) at the same round. `verified()`
9391041/// and `certified()` must write to distinct archives so both blocks are
0 commit comments