Skip to content

Commit 08fa5dc

Browse files
add regression test for pruning
1 parent e1f6db9 commit 08fa5dc

3 files changed

Lines changed: 113 additions & 0 deletions

File tree

consensus/src/marshal/coding/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,11 @@ mod tests {
264264
harness::certify_persists_equivocated_block::<CodingHarness>();
265265
}
266266

267+
#[test_traced("WARN")]
268+
fn test_coding_certify_at_later_view_survives_earlier_view_pruning() {
269+
harness::certify_at_later_view_survives_earlier_view_pruning::<CodingHarness>();
270+
}
271+
267272
/// Regression test for issue #167: finalizing a descendant must not
268273
/// height-prune the shard-engine buffer before `try_repair_gaps` has
269274
/// consumed buffer-only ancestors.

consensus/src/marshal/mocks/harness.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

consensus/src/marshal/standard/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,12 @@ mod tests {
207207
harness::certify_persists_equivocated_block::<DeferredHarness>();
208208
}
209209

210+
#[test_traced("WARN")]
211+
fn test_standard_certify_at_later_view_survives_earlier_view_pruning() {
212+
harness::certify_at_later_view_survives_earlier_view_pruning::<InlineHarness>();
213+
harness::certify_at_later_view_survives_earlier_view_pruning::<DeferredHarness>();
214+
}
215+
210216
#[test_traced("WARN")]
211217
fn test_standard_delivery_visibility_implies_recoverable_after_restart() {
212218
harness::delivery_visibility_implies_recoverable_after_restart::<InlineHarness>(0..16);

0 commit comments

Comments
 (0)