@@ -21,6 +21,7 @@ import (
2121 "github.com/evstack/ev-node/pkg/genesis"
2222 seqcommon "github.com/evstack/ev-node/pkg/sequencers/common"
2323 "github.com/evstack/ev-node/pkg/store"
24+ "github.com/evstack/ev-node/types"
2425)
2526
2627// ErrInvalidId is returned when the chain id is invalid
@@ -57,6 +58,11 @@ type Sequencer struct {
5758 // inclusion transactions, no mempool) before resuming normal sequencing.
5859 // This ensures the sequencer produces the same blocks that nodes running in
5960 // base sequencing mode would have produced during the downtime.
61+ //
62+ // catchingUp is true when the sequencer is replaying missed DA epochs.
63+ // It is set when we detect (via GetLatestDAHeight) that the DA layer is more
64+ // than one epoch ahead of our checkpoint, and cleared when we hit
65+ // ErrHeightFromFuture (meaning we've reached the DA head).
6066 catchingUp bool
6167 // currentDAEndTime is the DA epoch end timestamp from the last fetched epoch.
6268 // Used as the block timestamp during catch-up to match based sequencing behavior.
@@ -421,14 +427,20 @@ func (c *Sequencer) IsCatchingUp() bool {
421427}
422428
423429// fetchNextDAEpoch fetches transactions from the next DA epoch using checkpoint.
424- // It also updates the catch-up state based on the DA epoch timestamp:
425- // - If the fetched epoch's timestamp is significantly in the past (more than
426- // one epoch's wall-clock duration), the sequencer enters catch-up mode.
430+ // It also updates the catch-up state based on DA heights:
431+ // - Before the first fetch, it queries GetLatestDAHeight to determine if the
432+ // sequencer has missed more than one DA epoch. If so, catch-up mode is
433+ // entered and only forced-inclusion blocks (no mempool) are produced.
427434// - If the DA height is from the future (not yet produced), the sequencer
428435// exits catch-up mode as it has reached the DA head.
429436func (c * Sequencer ) fetchNextDAEpoch (ctx context.Context , maxBytes uint64 ) (uint64 , error ) {
430437 currentDAHeight := c .checkpoint .DAHeight
431438
439+ // Determine catch-up state before the (potentially expensive) epoch fetch.
440+ // This is done once per sequencer lifecycle — subsequent catch-up exits are
441+ // handled by ErrHeightFromFuture below.
442+ c .updateCatchUpState (ctx )
443+
432444 c .logger .Debug ().
433445 Uint64 ("da_height" , currentDAHeight ).
434446 Uint64 ("tx_index" , c .checkpoint .TxIndex ).
@@ -466,11 +478,6 @@ func (c *Sequencer) fetchNextDAEpoch(ctx context.Context, maxBytes uint64) (uint
466478 c .currentDAEndTime = forcedTxsEvent .Timestamp .UTC ()
467479 }
468480
469- // Determine catch-up state based on epoch timestamp.
470- // If the epoch we just fetched ended more than one epoch's wall-clock duration ago,
471- // we are behind the DA head and must catch up by replaying missed epochs.
472- c .updateCatchUpState (forcedTxsEvent )
473-
474481 // Validate and filter transactions
475482 validTxs := make ([][]byte , 0 , len (forcedTxsEvent .Txs ))
476483 skippedTxs := 0
@@ -501,60 +508,75 @@ func (c *Sequencer) fetchNextDAEpoch(ctx context.Context, maxBytes uint64) (uint
501508 return forcedTxsEvent .EndDaHeight , nil
502509}
503510
504- // updateCatchUpState determines whether the sequencer is catching up to the DA head.
511+ // updateCatchUpState determines whether the sequencer needs to catch up to the
512+ // DA head by comparing the sequencer's checkpoint DA height against the latest
513+ // DA height.
505514//
506- // The sequencer is considered to be catching up when the DA epoch it just fetched
507- // has a timestamp that is significantly in the past — specifically, more than one
508- // full epoch's wall-clock duration ago. This means other nodes likely switched to
509- // base sequencing during the sequencer's downtime, and the sequencer must replay
510- // those missed epochs before resuming normal block production.
515+ // The detection is purely height-based: we query GetLatestDAHeight once (on the
516+ // first epoch fetch) and calculate how many epochs the sequencer has missed. If
517+ // the gap exceeds one epoch, the sequencer enters catch-up mode and replays
518+ // missed epochs with forced-inclusion transactions only (no mempool). It remains
519+ // in catch-up until fetchNextDAEpoch hits ErrHeightFromFuture, meaning we've
520+ // reached the DA head.
511521//
512- // When the epoch timestamp is recent (within one epoch duration), the sequencer
513- // has reached the DA head and can resume normal operation.
514- func ( c * Sequencer ) updateCatchUpState ( event * block. ForcedInclusionEvent ) {
515- if event == nil || event . Timestamp . IsZero () {
516- // No timestamp available (e.g., empty epoch) — don't change catch-up state.
517- // If we were already catching up, we remain in that state until we see a
518- // recent timestamp or hit HeightFromFuture.
522+ // This check is performed only once per sequencer lifecycle. If the downtime was
523+ // short enough that the sequencer is still within the current or next epoch, no
524+ // catch-up is needed and the (lightweight) GetLatestDAHeight call is the only
525+ // overhead.
526+ func ( c * Sequencer ) updateCatchUpState ( ctx context. Context ) {
527+ // Already catching up — nothing to do. We'll exit via ErrHeightFromFuture.
528+ if c . catchingUp {
519529 return
520530 }
521531
522- if c .genesis .DAEpochForcedInclusion == 0 {
532+ epochSize := c .genesis .DAEpochForcedInclusion
533+ if epochSize == 0 {
523534 // No epoch-based forced inclusion configured — catch-up is irrelevant.
524- c .catchingUp = false
525535 return
526536 }
527537
528- // Calculate how long one DA epoch takes in wall-clock time.
529- epochWallDuration := time .Duration (c .genesis .DAEpochForcedInclusion ) * c .cfg .DA .BlockTime .Duration
538+ currentDAHeight := c .checkpoint .DAHeight
539+ daStartHeight := c .genesis .DAStartHeight
540+
541+ latestDAHeight , err := c .daClient .GetLatestDAHeight (ctx )
542+ if err != nil {
543+ c .logger .Warn ().Err (err ).
544+ Msg ("failed to get latest DA height for catch-up detection, skipping check" )
545+ return
546+ }
530547
531- // Use a minimum threshold to avoid false positives from minor delays.
532- catchUpThreshold := epochWallDuration
533- if catchUpThreshold < 30 * time .Second {
534- catchUpThreshold = 30 * time .Second
548+ if latestDAHeight <= currentDAHeight {
549+ // DA hasn't moved beyond our position — nothing to catch up.
550+ c .logger .Debug ().
551+ Uint64 ("checkpoint_da_height" , currentDAHeight ).
552+ Uint64 ("latest_da_height" , latestDAHeight ).
553+ Msg ("sequencer is at or ahead of DA head, no catch-up needed" )
554+ return
535555 }
536556
537- timeSinceEpoch := time .Since (event .Timestamp )
538- wasCatchingUp := c .catchingUp
557+ // Calculate epoch numbers for current position and DA head.
558+ currentEpoch := types .CalculateEpochNumber (currentDAHeight , daStartHeight , epochSize )
559+ latestEpoch := types .CalculateEpochNumber (latestDAHeight , daStartHeight , epochSize )
560+ missedEpochs := latestEpoch - currentEpoch
539561
540- if timeSinceEpoch > catchUpThreshold {
541- c .catchingUp = true
542- if ! wasCatchingUp {
543- c .logger .Warn ().
544- Dur ("time_since_epoch" , timeSinceEpoch ).
545- Dur ("threshold" , catchUpThreshold ).
546- Uint64 ("epoch_start" , event .StartDaHeight ).
547- Uint64 ("epoch_end" , event .EndDaHeight ).
548- Msg ("entering catch-up mode: DA epoch is behind head, replaying missed epochs with forced inclusion txs only" )
549- }
550- } else {
551- c .catchingUp = false
552- if wasCatchingUp {
553- c .logger .Info ().
554- Dur ("time_since_epoch" , timeSinceEpoch ).
555- Uint64 ("epoch_start" , event .StartDaHeight ).
556- Uint64 ("epoch_end" , event .EndDaHeight ).
557- Msg ("exiting catch-up mode: reached DA head, resuming normal sequencing" )
558- }
562+ if missedEpochs <= 1 {
563+ // Within the current or next epoch — normal operation, no catch-up.
564+ c .logger .Debug ().
565+ Uint64 ("checkpoint_da_height" , currentDAHeight ).
566+ Uint64 ("latest_da_height" , latestDAHeight ).
567+ Uint64 ("current_epoch" , currentEpoch ).
568+ Uint64 ("latest_epoch" , latestEpoch ).
569+ Msg ("sequencer within one epoch of DA head, no catch-up needed" )
570+ return
559571 }
572+
573+ // The DA layer is more than one epoch ahead. Enter catch-up mode.
574+ c .catchingUp = true
575+ c .logger .Warn ().
576+ Uint64 ("checkpoint_da_height" , currentDAHeight ).
577+ Uint64 ("latest_da_height" , latestDAHeight ).
578+ Uint64 ("current_epoch" , currentEpoch ).
579+ Uint64 ("latest_epoch" , latestEpoch ).
580+ Uint64 ("missed_epochs" , missedEpochs ).
581+ Msg ("entering catch-up mode: DA layer is multiple epochs ahead, replaying missed epochs with forced inclusion txs only" )
560582}
0 commit comments