@@ -64,6 +64,8 @@ type Sequencer struct {
6464 catchUpState atomic.Int32
6565 // currentDAEndTime is the DA epoch end timestamp, used during catch-up
6666 currentDAEndTime time.Time
67+ // currentEpochTxCount is the total number of txs in the current DA epoch (used for timestamp jitter)
68+ currentEpochTxCount uint64
6769}
6870
6971// NewSequencer creates a new Single Sequencer
@@ -311,10 +313,14 @@ func (c *Sequencer) GetNextBatch(ctx context.Context, req coresequencer.GetNextB
311313 }
312314 }
313315
314- // Update checkpoint after consuming forced inclusion transactions
316+ // Update checkpoint after consuming forced inclusion transactions.
317+ // txIndexForTimestamp is captured before the epoch-boundary reset so the
318+ // final block of an epoch lands exactly on daEndTime.
319+ var txIndexForTimestamp uint64
315320 if daHeight > 0 || len (forcedTxs ) > 0 {
316321 // Advance TxIndex by the number of consumed forced transactions
317322 c .checkpoint .TxIndex += forcedTxConsumedCount
323+ txIndexForTimestamp = c .checkpoint .TxIndex
318324
319325 if c .checkpoint .TxIndex >= uint64 (len (c .cachedForcedInclusionTxs )) {
320326 // All forced txs were consumed (OK or Remove), move to next DA epoch
@@ -341,14 +347,16 @@ func (c *Sequencer) GetNextBatch(ctx context.Context, req coresequencer.GetNextB
341347 batchTxs = append (batchTxs , validForcedTxs ... )
342348 batchTxs = append (batchTxs , validMempoolTxs ... )
343349
344- // Use DA epoch timestamp during catch-up
350+ // Spread catch-up blocks across the DA epoch window for monotonically increasing timestamps:
351+ // epochStart = daEndTime - totalEpochTxs * 1ms
352+ // blockTimestamp = epochStart + txIndexForTimestamp * 1ms
353+ // The last block of an epoch lands exactly on daEndTime; the first block of
354+ // the next epoch starts at nextDaEndTime - N*1ms >= prevDaEndTime.
355+ // During normal operation, use wall-clock time instead.
345356 timestamp := time .Now ()
346357 if c .catchUpState .Load () == catchUpInProgress && ! c .currentDAEndTime .IsZero () {
347- var remainingForcedTxs uint64
348- if len (c .cachedForcedInclusionTxs ) > 0 {
349- remainingForcedTxs = uint64 (len (c .cachedForcedInclusionTxs )) - c .checkpoint .TxIndex
350- }
351- timestamp = c .currentDAEndTime .Add (- time .Duration (remainingForcedTxs ) * time .Millisecond )
358+ epochStart := c .currentDAEndTime .Add (- time .Duration (c .currentEpochTxCount ) * time .Millisecond )
359+ timestamp = epochStart .Add (time .Duration (txIndexForTimestamp ) * time .Millisecond )
352360 }
353361
354362 return & coresequencer.GetNextBatchResponse {
@@ -447,6 +455,8 @@ func (c *Sequencer) fetchNextDAEpoch(ctx context.Context, maxBytes uint64) (uint
447455 if ! forcedTxsEvent .Timestamp .IsZero () {
448456 c .currentDAEndTime = forcedTxsEvent .Timestamp .UTC ()
449457 }
458+ // Record total tx count for the epoch so the timestamp jitter can be computed
459+ // after oversized txs are filtered out below.
450460
451461 // Filter out oversized transactions
452462 validTxs := make ([][]byte , 0 , len (forcedTxsEvent .Txs ))
@@ -473,6 +483,7 @@ func (c *Sequencer) fetchNextDAEpoch(ctx context.Context, maxBytes uint64) (uint
473483 Msg ("fetched forced inclusion transactions from DA" )
474484
475485 c .cachedForcedInclusionTxs = validTxs
486+ c .currentEpochTxCount = uint64 (len (validTxs ))
476487
477488 return forcedTxsEvent .EndDaHeight , nil
478489}
0 commit comments