@@ -14,7 +14,7 @@ use hotshot_types::{
1414 consensus:: OuterConsensus ,
1515 data:: { EpochNumber , ViewNumber } ,
1616 epoch_membership:: EpochMembershipCoordinator ,
17- event:: Event ,
17+ event:: { Event , EventType } ,
1818 message:: UpgradeLock ,
1919 simple_certificate:: { NextEpochQuorumCertificate2 , QuorumCertificate2 , TimeoutCertificate2 } ,
2020 simple_vote:: { HasEpoch , NextEpochQuorumVote2 , QuorumVote2 , TimeoutVote2 } ,
@@ -35,7 +35,7 @@ use self::handlers::{
3535} ;
3636use crate :: {
3737 events:: HotShotEvent ,
38- helpers:: { broadcast_view_change, validate_qc_and_next_epoch_qc} ,
38+ helpers:: { broadcast_event , broadcast_view_change, validate_qc_and_next_epoch_qc} ,
3939 vote_collection:: { EpochRootVoteCollectorsMap , VoteCollectorsMap } ,
4040} ;
4141
@@ -261,6 +261,32 @@ impl<TYPES: NodeType, I: NodeImplementation<TYPES>> ConsensusTaskState<TYPES, I>
261261 . await ;
262262 }
263263 } ,
264+ HotShotEvent :: Qc2Formed ( either:: Left ( qc) )
265+ if self . upgrade_lock . new_protocol_active ( self . cur_view )
266+ && !self . upgrade_lock . new_protocol_active ( qc. view_number ( ) ) =>
267+ {
268+ // Cutover boundary only: the gated proposal path won't land this
269+ // last-legacy QC in `high_qc`, so capture it here for
270+ // `extract_pre_cutover_seed` to carry across. `update_high_qc` is
271+ // monotone, so this is a no-op outside the cutover window.
272+ let mut consensus_writer = self . consensus . write ( ) . await ;
273+ let _ = consensus_writer. update_high_qc ( qc. clone ( ) ) ;
274+ drop ( consensus_writer) ;
275+ if let Err ( e) = self . storage . update_high_qc2 ( qc. clone ( ) ) . await {
276+ tracing:: warn!( "Failed to persist boundary high QC: {e}" ) ;
277+ }
278+ // Forward to the espresso bridge -> new-protocol coordinator, in case the
279+ // cutover seed was snapshotted before this QC finished assembling. Only the
280+ // cutover-view leader reaches this arm, so it lands exactly where needed.
281+ broadcast_event (
282+ Event {
283+ view_number : qc. view_number ( ) ,
284+ event : EventType :: LegacyHighQcFormed { qc : qc. clone ( ) } ,
285+ } ,
286+ & self . output_event_stream ,
287+ )
288+ . await ;
289+ } ,
264290 _ => { } ,
265291 }
266292
@@ -279,7 +305,24 @@ impl<TYPES: NodeType, I: NodeImplementation<TYPES>> TaskState for ConsensusTaskS
279305 _receiver : & Receiver < Arc < Self :: Event > > ,
280306 ) -> Result < ( ) > {
281307 if self . upgrade_lock . new_protocol_active ( self . cur_view ) {
282- return Ok ( ( ) ) ;
308+ // Past cutover: still admit votes/QCs for strictly-pre-cutover views so
309+ // the cutover leader can finish the last legacy QC; everything else
310+ // (proposing, voting, view changes) stays shut down.
311+ let admit = match event. as_ref ( ) {
312+ HotShotEvent :: QuorumVoteRecv ( vote) => {
313+ !self . upgrade_lock . new_protocol_active ( vote. view_number ( ) )
314+ } ,
315+ HotShotEvent :: EpochRootQuorumVoteRecv ( vote) => {
316+ !self . upgrade_lock . new_protocol_active ( vote. view_number ( ) )
317+ } ,
318+ HotShotEvent :: Qc2Formed ( either:: Left ( qc) ) => {
319+ !self . upgrade_lock . new_protocol_active ( qc. view_number ( ) )
320+ } ,
321+ _ => false ,
322+ } ;
323+ if !admit {
324+ return Ok ( ( ) ) ;
325+ }
283326 }
284327 self . handle ( event, sender. clone ( ) ) . await
285328 }
0 commit comments