@@ -259,6 +259,13 @@ pub trait TestHarness: 'static + Sized {
259259 all_handles : & mut [ ValidatorHandle < Self > ] ,
260260 ) -> impl Future < Output = ( ) > + Send ;
261261
262+ /// Mark a block as certified (notarized) via the mailbox.
263+ fn certify (
264+ handle : & mut ValidatorHandle < Self > ,
265+ round : Round ,
266+ block : & Self :: TestBlock ,
267+ ) -> impl Future < Output = bool > + Send ;
268+
262269 /// Create a finalization certificate.
263270 fn make_finalization (
264271 proposal : Proposal < Self :: Commitment > ,
@@ -927,6 +934,81 @@ pub fn verified_success_implies_recoverable_after_restart<H: TestHarness>(
927934 }
928935}
929936
937+ /// Regression: when a leader equivocates, a validator may verify one block
938+ /// (A) and then certify a different block (B) at the same round. `verified()`
939+ /// and `certified()` must write to distinct archives so both blocks are
940+ /// retained and retrievable; otherwise the second write collides on the same
941+ /// prunable-archive index (`skip_if_index_exists=true`) and is silently
942+ /// dropped despite the mailbox returning success.
943+ pub fn certify_persists_equivocated_block < H : TestHarness > ( ) {
944+ let runner = deterministic:: Runner :: timed ( Duration :: from_secs ( 60 ) ) ;
945+ runner. start ( |mut context| async move {
946+ let Fixture {
947+ participants,
948+ schemes,
949+ ..
950+ } = bls12381_threshold_vrf:: fixture :: < V , _ > ( & mut context, NAMESPACE , NUM_VALIDATORS ) ;
951+ let mut oracle =
952+ setup_network_with_participants ( context. clone ( ) , NZUsize ! ( 1 ) , participants. clone ( ) )
953+ . await ;
954+ let setup = H :: setup_validator (
955+ context. with_label ( "validator_0" ) ,
956+ & mut oracle,
957+ participants[ 0 ] . clone ( ) ,
958+ ConstantProvider :: new ( schemes[ 0 ] . clone ( ) ) ,
959+ )
960+ . await ;
961+ let mut handle = ValidatorHandle :: < H > {
962+ mailbox : setup. mailbox ,
963+ extra : setup. extra ,
964+ } ;
965+
966+ let round = Round :: new ( Epoch :: zero ( ) , View :: new ( 1 ) ) ;
967+ let parent = Sha256 :: hash ( b"" ) ;
968+ let parent_commitment = H :: genesis_parent_commitment ( NUM_VALIDATORS as u16 ) ;
969+
970+ // Two distinct blocks at the same height/round (leader equivocation):
971+ // distinct timestamps yield distinct digests.
972+ let block_a = H :: make_test_block (
973+ parent,
974+ parent_commitment,
975+ Height :: new ( 1 ) ,
976+ 1 ,
977+ NUM_VALIDATORS as u16 ,
978+ ) ;
979+ let digest_a = H :: digest ( & block_a) ;
980+ let block_b = H :: make_test_block (
981+ parent,
982+ parent_commitment,
983+ Height :: new ( 1 ) ,
984+ 2 ,
985+ NUM_VALIDATORS as u16 ,
986+ ) ;
987+ let digest_b = H :: digest ( & block_b) ;
988+ assert_ne ! ( digest_a, digest_b, "test requires distinct digests" ) ;
989+
990+ let mut peers: [ ValidatorHandle < H > ; 0 ] = [ ] ;
991+ H :: verify ( & mut handle, round, & block_a, & mut peers) . await ;
992+ assert ! (
993+ H :: certify( & mut handle, round, & block_b) . await ,
994+ "certified must ack"
995+ ) ;
996+
997+ let got_a = handle. mailbox . get_block ( & digest_a) . await ;
998+ assert ! (
999+ got_a. is_some( ) ,
1000+ "verified block A must be persisted in verified_blocks"
1001+ ) ;
1002+ assert_eq ! ( got_a. unwrap( ) . digest( ) , digest_a) ;
1003+ let got_b = handle. mailbox . get_block ( & digest_b) . await ;
1004+ assert ! (
1005+ got_b. is_some( ) ,
1006+ "certified block B must be persisted despite a verify at the same round"
1007+ ) ;
1008+ assert_eq ! ( got_b. unwrap( ) . digest( ) , digest_b) ;
1009+ } ) ;
1010+ }
1011+
9301012/// Contract: once marshal has delivered a finalized block to the application,
9311013/// that finalized block and its certificate must already be durable.
9321014pub fn delivery_visibility_implies_recoverable_after_restart < H : TestHarness > (
@@ -1275,6 +1357,10 @@ impl TestHarness for StandardHarness {
12751357 assert ! ( handle. mailbox. verified( round, block. clone( ) ) . await ) ;
12761358 }
12771359
1360+ async fn certify ( handle : & mut ValidatorHandle < Self > , round : Round , block : & B ) -> bool {
1361+ handle. mailbox . certified ( round, block. clone ( ) ) . await
1362+ }
1363+
12781364 fn make_finalization ( proposal : Proposal < D > , schemes : & [ S ] , quorum : u32 ) -> Finalization < S , D > {
12791365 let finalizes: Vec < _ > = schemes
12801366 . iter ( )
@@ -1536,6 +1622,22 @@ impl TestHarness for InlineHarness {
15361622 . await ;
15371623 }
15381624
1625+ async fn certify (
1626+ handle : & mut ValidatorHandle < Self > ,
1627+ round : Round ,
1628+ block : & Self :: TestBlock ,
1629+ ) -> bool {
1630+ StandardHarness :: certify (
1631+ & mut ValidatorHandle :: < StandardHarness > {
1632+ mailbox : handle. mailbox . clone ( ) ,
1633+ extra : handle. extra . clone ( ) ,
1634+ } ,
1635+ round,
1636+ block,
1637+ )
1638+ . await
1639+ }
1640+
15391641 fn make_finalization (
15401642 proposal : Proposal < Self :: Commitment > ,
15411643 schemes : & [ S ] ,
@@ -1724,6 +1826,22 @@ impl TestHarness for DeferredHarness {
17241826 . await ;
17251827 }
17261828
1829+ async fn certify (
1830+ handle : & mut ValidatorHandle < Self > ,
1831+ round : Round ,
1832+ block : & Self :: TestBlock ,
1833+ ) -> bool {
1834+ InlineHarness :: certify (
1835+ & mut ValidatorHandle :: < InlineHarness > {
1836+ mailbox : handle. mailbox . clone ( ) ,
1837+ extra : handle. extra . clone ( ) ,
1838+ } ,
1839+ round,
1840+ block,
1841+ )
1842+ . await
1843+ }
1844+
17271845 fn make_finalization (
17281846 proposal : Proposal < Self :: Commitment > ,
17291847 schemes : & [ S ] ,
@@ -2063,6 +2181,14 @@ impl TestHarness for CodingHarness {
20632181 assert ! ( handle. mailbox. verified( round, block. clone( ) ) . await ) ;
20642182 }
20652183
2184+ async fn certify (
2185+ handle : & mut ValidatorHandle < Self > ,
2186+ round : Round ,
2187+ block : & CodedBlock < CodingB , ReedSolomon < Sha256 > , Sha256 > ,
2188+ ) -> bool {
2189+ handle. mailbox . certified ( round, block. clone ( ) ) . await
2190+ }
2191+
20662192 fn make_finalization (
20672193 proposal : Proposal < Commitment > ,
20682194 schemes : & [ S ] ,
0 commit comments