@@ -370,6 +370,7 @@ func (cs *State) Run(ctx context.Context) error {
370370 // we need the timeoutRoutine for replay so
371371 // we don't block on the tick chan.
372372 s .SpawnNamed ("timeoutTicker" , func () error { return cs .timeoutTicker .Run (ctx ) })
373+ s .SpawnNamed ("finalizerRoutine" , func () error { return cs .blockExec .StartFinalizer (ctx ) })
373374
374375 // We may have lost some votes if the process crashed reload from consensus
375376 // log to catchup.
@@ -696,7 +697,31 @@ func (cs *State) updateToState(state sm.State) {
696697 }
697698
698699 // Reset fields based on state.
700+ // Next desired block height
701+ height := state .LastBlockHeight + 1
702+ if height == 1 {
703+ height = state .InitialHeight
704+ }
705+
706+ // Apply decoupled delay: use validator set from height N-delay (floored at initial height)
707+ // TODO: revisit how validator/evidence data should be cached once execution delay becomes dynamic.
699708 validators := state .Validators
709+ if sm .DecoupleDelay > 0 {
710+ delayedHeight := height - int64 (sm .DecoupleDelay )
711+ if delayedHeight < state .InitialHeight {
712+ delayedHeight = state .InitialHeight
713+ }
714+ // Only load from store if we have blocks saved (not at genesis)
715+ if state .LastBlockHeight > 0 && delayedHeight >= state .InitialHeight {
716+ delayedValidators , err := cs .stateStore .LoadValidators (delayedHeight )
717+ if err != nil {
718+ cs .logger .Error ("failed to load delayed validators, using current" , "delayedHeight" , delayedHeight , "err" , err )
719+ } else {
720+ cs .logger .Info ("using decoupled validator set" , "height" , height , "validatorSetFromHeight" , delayedHeight , "delay" , sm .DecoupleDelay )
721+ validators = delayedValidators
722+ }
723+ }
724+ }
700725
701726 switch {
702727 case state .LastBlockHeight == 0 : // Very first commit should be empty.
@@ -720,12 +745,6 @@ func (cs *State) updateToState(state sm.State) {
720745 ))
721746 }
722747
723- // Next desired block height
724- height := state .LastBlockHeight + 1
725- if height == 1 {
726- height = state .InitialHeight
727- }
728-
729748 // RoundState fields
730749 cs .updateHeight (height )
731750 cs .updateRoundStep (0 , cstypes .RoundStepNewHeight )
@@ -757,7 +776,7 @@ func (cs *State) updateToState(state sm.State) {
757776 cs .roundState .SetLastValidators (state .LastValidators )
758777 cs .roundState .SetTriggeredTimeoutPrecommit (false )
759778
760- cs .state = state
779+ cs .state = state // <-- this is the state that was applied
761780
762781 // Finally, broadcast RoundState
763782 cs .newStep ()
@@ -1438,7 +1457,7 @@ func (cs *State) createProposalBlock(ctx context.Context) (block *types.Block, e
14381457
14391458 case cs .roundState .LastCommit ().HasTwoThirdsMajority ():
14401459 // Make the commit from LastCommit
1441- lastCommit = cs .roundState .LastCommit ().MakeCommit ()
1460+ lastCommit = cs .roundState .LastCommit ().MakeCommit () // we make it from roundState -> so we still need to be updating the roundState at the end of voting despite the execution not yet being complete.
14421461
14431462 default : // This shouldn't happen.
14441463 cs .logger .Error ("propose step; cannot propose anything without commit for the previous block" )
@@ -2083,6 +2102,7 @@ func (cs *State) finalizeCommit(ctx context.Context, height int64) {
20832102 block ,
20842103 cs .tracer ,
20852104 )
2105+ // we need to pull the side effects such as writing validators for a committed height out of the apply block function
20862106 cs .metrics .MarkApplyBlockLatency (time .Since (startTime ))
20872107 if err != nil {
20882108 logger .Error ("failed to apply block" , "err" , err )
0 commit comments