@@ -312,6 +312,28 @@ where
312312 . with_label ( "propose" )
313313 . with_attribute ( "round" , consensus_context. round )
314314 . spawn ( move |runtime_context| async move {
315+ // On leader recovery, marshal may already hold a verified
316+ // block for this round (persisted by a pre-crash propose
317+ // whose notarize vote never reached the journal). Building
318+ // a fresh block would land on the same view index in the
319+ // prunable archive and be silently dropped, so reuse the
320+ // stored block instead.
321+ if let Some ( block) = marshal. get_verified ( consensus_context. round ) . await {
322+ let digest = block. digest ( ) ;
323+ {
324+ let mut lock = last_built. lock ( ) ;
325+ * lock = Some ( ( consensus_context. round , block) ) ;
326+ }
327+ let success = tx. send_lossy ( digest) ;
328+ debug ! (
329+ round = ?consensus_context. round,
330+ ?digest,
331+ success,
332+ "reused verified block from marshal on leader recovery"
333+ ) ;
334+ return ;
335+ }
336+
315337 let ( parent_view, parent_digest) = consensus_context. parent ;
316338 let parent_request = fetch_parent (
317339 parent_digest,
@@ -683,9 +705,9 @@ mod tests {
683705 } ,
684706 verifying:: { GatedVerifyingApp , MockVerifyingApp } ,
685707 } ,
686- simplex:: scheme:: bls12381_threshold:: vrf as bls12381_threshold_vrf,
708+ simplex:: { scheme:: bls12381_threshold:: vrf as bls12381_threshold_vrf, Plan } ,
687709 types:: { Epoch , Epocher , FixedEpocher , Height , Round , View } ,
688- Automaton , CertifiableAutomaton ,
710+ Automaton , CertifiableAutomaton , Relay ,
689711 } ;
690712 use commonware_broadcast:: Broadcaster ;
691713 use commonware_cryptography:: {
@@ -1101,4 +1123,69 @@ mod tests {
11011123 }
11021124 } ) ;
11031125 }
1126+
1127+ /// Regression: when marshal holds a verified block for a round from a
1128+ /// pre-crash propose, a restarted leader's `propose` must return that
1129+ /// block's digest instead of asking the application to build afresh.
1130+ /// See `standard::inline::tests::test_propose_reuses_verified_block_on_restart`.
1131+ #[ test_traced( "WARN" ) ]
1132+ fn test_propose_reuses_verified_block_on_restart ( ) {
1133+ let runner = deterministic:: Runner :: timed ( Duration :: from_secs ( 30 ) ) ;
1134+ runner. start ( |mut context| async move {
1135+ let Fixture {
1136+ participants,
1137+ schemes,
1138+ ..
1139+ } = bls12381_threshold_vrf:: fixture :: < V , _ > ( & mut context, NAMESPACE , NUM_VALIDATORS ) ;
1140+ let mut oracle =
1141+ setup_network_with_participants ( context. clone ( ) , NZUsize ! ( 1 ) , participants. clone ( ) )
1142+ . await ;
1143+
1144+ let me = participants[ 0 ] . clone ( ) ;
1145+ let setup = StandardHarness :: setup_validator (
1146+ context. with_label ( "validator_0" ) ,
1147+ & mut oracle,
1148+ me. clone ( ) ,
1149+ ConstantProvider :: new ( schemes[ 0 ] . clone ( ) ) ,
1150+ )
1151+ . await ;
1152+ let marshal = setup. mailbox ;
1153+
1154+ let genesis = make_raw_block ( Sha256 :: hash ( b"" ) , Height :: zero ( ) , 0 ) ;
1155+ let round = Round :: new ( Epoch :: zero ( ) , View :: new ( 1 ) ) ;
1156+ let ctx = Ctx {
1157+ round,
1158+ leader : me. clone ( ) ,
1159+ parent : ( View :: zero ( ) , genesis. digest ( ) ) ,
1160+ } ;
1161+ let block_a = B :: new :: < Sha256 > ( ctx. clone ( ) , genesis. digest ( ) , Height :: new ( 1 ) , 100 ) ;
1162+ let digest_a = block_a. digest ( ) ;
1163+ assert ! ( marshal. proposed( round, block_a. clone( ) ) . await ) ;
1164+
1165+ let block_b = B :: new :: < Sha256 > ( ctx. clone ( ) , genesis. digest ( ) , Height :: new ( 1 ) , 200 ) ;
1166+ let digest_b = block_b. digest ( ) ;
1167+ assert_ne ! ( digest_a, digest_b, "test requires distinct digests" ) ;
1168+
1169+ let mock_app: MockVerifyingApp < B , S > =
1170+ MockVerifyingApp :: new ( genesis. clone ( ) ) . with_propose_result ( block_b) ;
1171+ let mut marshaled = Deferred :: new (
1172+ context. clone ( ) ,
1173+ mock_app,
1174+ marshal. clone ( ) ,
1175+ FixedEpocher :: new ( BLOCKS_PER_EPOCH ) ,
1176+ ) ;
1177+
1178+ let digest_rx = marshaled. propose ( ctx) . await ;
1179+ let digest = digest_rx. await . expect ( "propose must return a digest" ) ;
1180+ assert_eq ! (
1181+ digest, digest_a,
1182+ "propose must reuse the block marshal already persisted for this round"
1183+ ) ;
1184+
1185+ assert ! (
1186+ marshaled. broadcast( digest_a, Plan :: Propose ) . await ,
1187+ "relay broadcast must succeed after re-propose"
1188+ ) ;
1189+ } ) ;
1190+ }
11041191}
0 commit comments