Skip to content

Commit 3274ca2

Browse files
committed
fixes
1 parent 6cea6c8 commit 3274ca2

11 files changed

Lines changed: 101 additions & 75 deletions

File tree

block/internal/da/async_block_retriever.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func (f *asyncBlockRetriever) GetCachedBlock(ctx context.Context, daHeight uint6
157157

158158
block := &BlockData{
159159
Height: pbBlock.Height,
160-
Timestamp: time.Unix(pbBlock.Timestamp, 0).UTC(),
160+
Timestamp: time.Unix(0, pbBlock.Timestamp).UTC(),
161161
Blobs: pbBlock.Blobs,
162162
}
163163

@@ -261,7 +261,7 @@ func (f *asyncBlockRetriever) fetchAndCacheBlock(height uint64) {
261261
// Serialize and cache the block
262262
pbBlock := &pb.BlockData{
263263
Height: block.Height,
264-
Timestamp: block.Timestamp.Unix(),
264+
Timestamp: block.Timestamp.UnixNano(),
265265
Blobs: block.Blobs,
266266
}
267267
data, err := proto.Marshal(pbBlock)

block/internal/da/async_block_retriever_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ func TestAsyncBlockRetriever_StopGracefully(t *testing.T) {
201201
func TestBlockData_Serialization(t *testing.T) {
202202
block := &BlockData{
203203
Height: 100,
204-
Timestamp: time.Unix(12345, 0).UTC(),
204+
Timestamp: time.Unix(12345, 123456789).UTC(),
205205
Blobs: [][]byte{
206206
[]byte("blob1"),
207207
[]byte("blob2"),
@@ -212,7 +212,7 @@ func TestBlockData_Serialization(t *testing.T) {
212212
// Serialize using protobuf
213213
pbBlock := &pb.BlockData{
214214
Height: block.Height,
215-
Timestamp: block.Timestamp.Unix(),
215+
Timestamp: block.Timestamp.UnixNano(),
216216
Blobs: block.Blobs,
217217
}
218218
data, err := proto.Marshal(pbBlock)
@@ -226,11 +226,11 @@ func TestBlockData_Serialization(t *testing.T) {
226226

227227
decoded := &BlockData{
228228
Height: decodedPb.Height,
229-
Timestamp: time.Unix(decodedPb.Timestamp, 0).UTC(),
229+
Timestamp: time.Unix(0, decodedPb.Timestamp).UTC(),
230230
Blobs: decodedPb.Blobs,
231231
}
232232

233-
assert.Equal(t, block.Timestamp.Unix(), decoded.Timestamp.Unix())
233+
assert.Equal(t, block.Timestamp.UnixNano(), decoded.Timestamp.UnixNano())
234234
assert.Equal(t, block.Height, decoded.Height)
235235
assert.Equal(t, len(block.Blobs), len(decoded.Blobs))
236236
for i := range block.Blobs {
@@ -248,7 +248,7 @@ func TestBlockData_SerializationEmpty(t *testing.T) {
248248
// Serialize using protobuf
249249
pbBlock := &pb.BlockData{
250250
Height: block.Height,
251-
Timestamp: block.Timestamp.Unix(),
251+
Timestamp: block.Timestamp.UnixNano(),
252252
Blobs: block.Blobs,
253253
}
254254
data, err := proto.Marshal(pbBlock)
@@ -261,7 +261,7 @@ func TestBlockData_SerializationEmpty(t *testing.T) {
261261

262262
decoded := &BlockData{
263263
Height: decodedPb.Height,
264-
Timestamp: time.Unix(decodedPb.Timestamp, 0).UTC(),
264+
Timestamp: time.Unix(0, decodedPb.Timestamp).UTC(),
265265
Blobs: decodedPb.Blobs,
266266
}
267267

block/internal/syncing/syncer.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,19 @@ func (s *Syncer) initializeState() error {
309309
}
310310
s.SetLastState(state)
311311

312+
// Initialize lastCheckedEpochEnd based on the restored state's DA height so that
313+
// VerifyForcedInclusionTxs resumes from where we left off instead of re-scanning
314+
// all epochs from genesis on every startup.
315+
if epochSize := s.genesis.DAEpochForcedInclusion; epochSize > 0 && state.DAHeight >= s.genesis.DAStartHeight {
316+
firstEpochEnd := s.genesis.DAStartHeight + epochSize - 1
317+
if state.DAHeight >= firstEpochEnd {
318+
// The last completed epoch end that is fully behind state.DAHeight.
319+
elapsed := state.DAHeight - firstEpochEnd
320+
completedEpochs := elapsed / epochSize
321+
s.lastCheckedEpochEnd = firstEpochEnd + completedEpochs*epochSize
322+
}
323+
}
324+
312325
// Set DA height to the maximum of the genesis start height, the state's DA height, and the cached DA height.
313326
// The cache's DaHeight() is initialized from store metadata, so it's always correct even after cache clear.
314327
s.daRetrieverHeight.Store(max(s.genesis.DAStartHeight, s.cache.DaHeight(), state.DAHeight))
@@ -915,7 +928,7 @@ func (s *Syncer) VerifyForcedInclusionTxs(ctx context.Context, daHeight uint64,
915928
s.daBlockBytes[daHeight] = blockBytes
916929
s.forcedInclusionMu.Unlock()
917930

918-
if daHeight < daStart {
931+
if daHeight < daStart || daHeight < s.getLastState().DAHeight {
919932
return nil
920933
}
921934

@@ -926,7 +939,17 @@ func (s *Syncer) VerifyForcedInclusionTxs(ctx context.Context, daHeight uint64,
926939

927940
var maliciousCount int
928941

929-
for epochEnd := daStart + epochSize - 1; ; epochEnd += epochSize {
942+
// Resume from the last checked epoch rather than re-scanning from genesis.
943+
// If no epoch has been checked yet, start from the first epoch end.
944+
firstEpochEnd := daStart + epochSize - 1
945+
var startEpochEnd uint64
946+
if s.lastCheckedEpochEnd == 0 || s.lastCheckedEpochEnd < firstEpochEnd {
947+
startEpochEnd = firstEpochEnd
948+
} else {
949+
startEpochEnd = s.lastCheckedEpochEnd + epochSize
950+
}
951+
952+
for epochEnd := startEpochEnd; ; epochEnd += epochSize {
930953
epochStart := epochEnd - (epochSize - 1)
931954
gracePeriod := s.gracePeriodForEpoch(epochStart, epochEnd)
932955
graceBoundary := epochEnd + gracePeriod*epochSize

pkg/sequencers/based/sequencer.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func NewBasedSequencer(
5959
logger: logger.With().Str("component", "based_sequencer").Logger(),
6060
checkpointStore: seqcommon.NewCheckpointStore(db, ds.NewKey("/based/checkpoint")),
6161
executor: executor,
62-
currentDAEndTime: genesis.StartTime,
62+
currentDAEndTime: genesis.StartTime.UTC(),
6363
}
6464

6565
// Read state from the store to allow nodes to restart as based sequencers on a chain that had ran previously with a different sequencer type, and to initialize the timestamp floor for monotonicity guarantees after restart.
@@ -210,13 +210,6 @@ doneProcessing:
210210
// the next epoch starts at nextDaEndTime - N*1ms >= prevDaEndTime.
211211
epochStart := s.currentDAEndTime.Add(-time.Duration(s.currentEpochTxCount) * time.Millisecond)
212212
timestamp := epochStart.Add(time.Duration(txIndexForTimestamp) * time.Millisecond)
213-
214-
// Clamp: the DA-derived timestamp may predate blocks that were
215-
// produced or synced with wall-clock time before the node restarted
216-
// as a based sequencer. Ensure strict monotonicity.
217-
if !s.lastTimestamp.IsZero() && !timestamp.After(s.lastTimestamp) {
218-
timestamp = s.lastTimestamp.Add(time.Millisecond)
219-
}
220213
s.lastTimestamp = timestamp
221214

222215
if len(validTxs) == 0 {

pkg/sequencers/based/sequencer_test.go

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,13 @@ func TestBasedSequencer_GetNextBatch_EmptyDA(t *testing.T) {
167167
LastBatchData: nil,
168168
}
169169

170+
// Empty epoch has no valid txs — sequencer returns ErrNoBatch
170171
resp, err := seq.GetNextBatch(context.Background(), req)
171-
require.NoError(t, err)
172-
require.NotNil(t, resp)
173-
require.NotNil(t, resp.Batch)
174-
// Should return empty batch when DA has no transactions
175-
assert.Equal(t, 0, len(resp.Batch.Transactions))
172+
require.ErrorIs(t, err, block.ErrNoBatch)
173+
require.Nil(t, resp)
174+
175+
// Checkpoint should still advance past the empty epoch
176+
assert.Equal(t, uint64(101), seq.checkpoint.DAHeight)
176177

177178
mockRetriever.AssertExpectations(t)
178179
}
@@ -339,12 +340,13 @@ func TestBasedSequencer_GetNextBatch_ForcedInclusionExceedsMaxBytes(t *testing.T
339340
LastBatchData: nil,
340341
}
341342

343+
// All txs are skipped due to size — sequencer returns ErrNoBatch
342344
resp, err := seq.GetNextBatch(context.Background(), req)
343-
require.NoError(t, err)
344-
require.NotNil(t, resp)
345-
require.NotNil(t, resp.Batch)
346-
// Should return empty batch since transaction exceeds max bytes
347-
assert.Equal(t, 0, len(resp.Batch.Transactions))
345+
require.ErrorIs(t, err, block.ErrNoBatch)
346+
require.Nil(t, resp)
347+
348+
// Checkpoint should still advance past the epoch with the oversized tx
349+
assert.Equal(t, uint64(101), seq.checkpoint.DAHeight)
348350

349351
mockRetriever.AssertExpectations(t)
350352
}
@@ -430,13 +432,12 @@ func TestBasedSequencer_GetNextBatch_HeightFromFuture(t *testing.T) {
430432
LastBatchData: nil,
431433
}
432434

433-
// Should not error, but return empty batch
435+
// DA hasn't produced that block yet — sequencer returns ErrNoBatch
434436
resp, err := seq.GetNextBatch(context.Background(), req)
435-
require.NoError(t, err)
436-
require.NotNil(t, resp)
437-
assert.Equal(t, 0, len(resp.Batch.Transactions))
437+
require.ErrorIs(t, err, block.ErrNoBatch)
438+
require.Nil(t, resp)
438439

439-
// DA height should stay the same
440+
// DA height should stay the same — checkpoint must not advance
440441
assert.Equal(t, uint64(100), seq.checkpoint.DAHeight)
441442

442443
mockRetriever.AssertExpectations(t)
@@ -665,24 +666,21 @@ func TestBasedSequencer_GetNextBatch_EmptyDABatch_IncreasesDAHeight(t *testing.T
665666
assert.Equal(t, uint64(100), seq.GetDAHeight())
666667
assert.Equal(t, uint64(100), seq.checkpoint.DAHeight)
667668

668-
// First batch - empty DA block at height 100
669+
// First call — empty DA epoch at height 100, no txs → ErrNoBatch.
670+
// Checkpoint must still advance so the next call moves to epoch 101.
669671
resp, err := seq.GetNextBatch(context.Background(), req)
670-
require.NoError(t, err)
671-
require.NotNil(t, resp)
672-
require.NotNil(t, resp.Batch)
673-
assert.Equal(t, 0, len(resp.Batch.Transactions))
672+
require.ErrorIs(t, err, block.ErrNoBatch)
673+
require.Nil(t, resp)
674674

675675
// DA height should have increased to 101 even though no transactions were processed
676676
assert.Equal(t, uint64(101), seq.GetDAHeight())
677677
assert.Equal(t, uint64(101), seq.checkpoint.DAHeight)
678678
assert.Equal(t, uint64(0), seq.checkpoint.TxIndex)
679679

680-
// Second batch - empty DA block at height 101
680+
// Second call — empty DA epoch at height 101, no txs → ErrNoBatch.
681681
resp, err = seq.GetNextBatch(context.Background(), req)
682-
require.NoError(t, err)
683-
require.NotNil(t, resp)
684-
require.NotNil(t, resp.Batch)
685-
assert.Equal(t, 0, len(resp.Batch.Transactions))
682+
require.ErrorIs(t, err, block.ErrNoBatch)
683+
require.Nil(t, resp)
686684

687685
// DA height should have increased to 102
688686
assert.Equal(t, uint64(102), seq.GetDAHeight())
@@ -801,7 +799,8 @@ func TestBasedSequencer_GetNextBatch_TimestampAdjustment_PartialBatch(t *testing
801799
}
802800

803801
func TestBasedSequencer_GetNextBatch_TimestampAdjustment_EmptyBatch(t *testing.T) {
804-
// Test that timestamp is zero when batch is empty
802+
// Test that an empty DA epoch returns ErrNoBatch (no valid txs to include).
803+
// The checkpoint must still advance past the empty epoch.
805804
daEndTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
806805

807806
mockRetriever := common.NewMockForcedInclusionRetriever(t)
@@ -825,15 +824,13 @@ func TestBasedSequencer_GetNextBatch_TimestampAdjustment_EmptyBatch(t *testing.T
825824
LastBatchData: nil,
826825
}
827826

827+
// Empty epoch — no valid txs, sequencer returns ErrNoBatch
828828
resp, err := seq.GetNextBatch(context.Background(), req)
829-
require.NoError(t, err)
830-
require.NotNil(t, resp)
831-
require.NotNil(t, resp.Batch)
832-
assert.Equal(t, 0, len(resp.Batch.Transactions))
829+
require.ErrorIs(t, err, block.ErrNoBatch)
830+
require.Nil(t, resp)
833831

834-
// When batch is empty, there are 0 remaining txs, so timestamp = daEndTime
835-
expectedTimestamp := daEndTime
836-
assert.Equal(t, expectedTimestamp, resp.Timestamp)
832+
// Checkpoint must have advanced so the next call moves past this epoch
833+
assert.Equal(t, uint64(101), seq.checkpoint.DAHeight)
837834

838835
mockRetriever.AssertExpectations(t)
839836
}

pkg/sequencers/single/sequencer.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func NewSequencer(
9393
queue: NewBatchQueue(db, "batches", maxQueueSize),
9494
checkpointStore: seqcommon.NewCheckpointStore(db, ds.NewKey("/single/checkpoint")),
9595
genesis: genesis,
96-
currentDAEndTime: genesis.StartTime,
96+
currentDAEndTime: genesis.StartTime.UTC(),
9797
executor: executor,
9898
}
9999
s.SetDAHeight(genesis.DAStartHeight) // default value, will be overridden by executor or submitter
@@ -358,20 +358,15 @@ func (c *Sequencer) GetNextBatch(ctx context.Context, req coresequencer.GetNextB
358358
// The last block of an epoch lands exactly on daEndTime; the first block of
359359
// the next epoch starts at nextDaEndTime - N*1ms >= prevDaEndTime.
360360
// During normal operation, use wall-clock time instead.
361-
timestamp := time.Now()
362-
if c.catchUpState.Load() == catchUpInProgress {
361+
timestamp := time.Now().UTC()
362+
currentBatchHasForcedTxs := forcedTxConsumedCount > 0
363+
if c.catchUpState.Load() == catchUpInProgress || currentBatchHasForcedTxs {
363364
epochStart := c.currentDAEndTime.Add(-time.Duration(c.currentEpochTxCount) * time.Millisecond)
364365
timestamp = epochStart.Add(time.Duration(txIndexForTimestamp) * time.Millisecond)
365-
366-
// Clamp: the DA-derived timestamp may predate blocks that were
367-
// produced with time.Now() before the sequencer was restarted.
368-
// Ensure strict monotonicity relative to the last produced block.
369-
if !c.lastCatchUpTimestamp.IsZero() && !timestamp.After(c.lastCatchUpTimestamp) {
370-
timestamp = c.lastCatchUpTimestamp.Add(time.Millisecond)
371-
}
372366
c.lastCatchUpTimestamp = timestamp
373367
}
374368

369+
// In catch up modes, only produce blocks for force included txs.
375370
if c.isCatchingUp() && len(batchTxs) == 0 {
376371
return nil, block.ErrNoBatch
377372
}

pkg/sequencers/single/sequencer_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2103,22 +2103,22 @@ func TestSequencer_CatchUp_MonotonicTimestamps_EmptyEpoch(t *testing.T) {
21032103
LastBatchData: nil,
21042104
}
21052105

2106-
// First call processes the empty epoch 100 — empty batch, but checkpoint advances
2107-
resp1, err := seq.GetNextBatch(ctx, req)
2108-
require.NoError(t, err)
2106+
// First call processes the empty epoch 100 — no txs while catching up → ErrNoBatch.
2107+
// The checkpoint must still advance to 101 so the next call moves on.
2108+
_, err = seq.GetNextBatch(ctx, req)
2109+
require.ErrorIs(t, err, block.ErrNoBatch, "empty catch-up epoch should return ErrNoBatch")
21092110
assert.True(t, seq.isCatchingUp())
2110-
assert.Equal(t, 0, len(resp1.Batch.Transactions), "empty epoch should produce empty batch")
2111-
assert.Equal(t, emptyEpochTimestamp, resp1.Timestamp,
2112-
"empty epoch batch should use epoch DA end time (0 remaining)")
2111+
assert.Equal(t, uint64(101), seq.checkpoint.DAHeight, "checkpoint should have advanced past empty epoch")
21132112

2114-
// Second call processes epoch 101 — should have later timestamp
2113+
// Second call processes epoch 101 — has a forced tx, should succeed with a
2114+
// DA-derived timestamp after emptyEpochTimestamp.
21152115
resp2, err := seq.GetNextBatch(ctx, req)
21162116
require.NoError(t, err)
21172117
assert.True(t, seq.isCatchingUp())
21182118
assert.Equal(t, 1, len(resp2.Batch.Transactions))
2119-
assert.True(t, resp2.Timestamp.After(resp1.Timestamp),
2119+
assert.True(t, resp2.Timestamp.After(emptyEpochTimestamp),
21202120
"epoch 101 timestamp (%v) must be after empty epoch 100 timestamp (%v)",
2121-
resp2.Timestamp, resp1.Timestamp)
2121+
resp2.Timestamp, emptyEpochTimestamp)
21222122
}
21232123

21242124
func TestSequencer_GetNextBatch_GasFilteringPreservesUnprocessedTxs(t *testing.T) {

proto/evnode/v1/da.proto

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ option go_package = "github.com/evstack/ev-node/types/pb/evnode/v1";
66
// BlockData contains data retrieved from a single DA height.
77
message BlockData {
88
uint64 height = 1;
9-
int64 timestamp = 2; // Unix timestamp in seconds
9+
int64 timestamp = 2; // Unix timestamp in nanoseconds
1010
repeated bytes blobs = 3;
1111
}

test/e2e/evm_force_inclusion_e2e_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,7 @@ func TestEvmSequencerCatchUpBasedSequencerE2E(t *testing.T) {
970970

971971
// ===== PHASE 10: Verify Nodes Are in Sync =====
972972
t.Log("Phase 10: Verify Nodes Are in Sync")
973+
time.Sleep(4 * time.Second) // hack to give some head start to the sequencer, otherwise it will pass immediatelly.
973974

974975
// Wait for sync node to catch up to sequencer
975976
require.Eventually(t, func() bool {
@@ -982,8 +983,8 @@ func TestEvmSequencerCatchUpBasedSequencerE2E(t *testing.T) {
982983
syncHeaderNb, seqHeaderNb := fnHeader.Number.Uint64(), seqHeader.Number.Uint64()
983984
t.Logf("Sync node height is %d and seq node height is %d", syncHeaderNb, seqHeaderNb)
984985

985-
return syncHeaderNb >= seqHeaderNb
986-
}, 30*time.Second, 1*time.Second, "Sync node should catch up to sequencer")
986+
return syncHeaderNb >= seqHeaderNb-10 // catching up within a 10 blocks range
987+
}, 15*time.Second, 1*time.Second, "Sync node should catch up to sequencer")
987988

988989
// Verify both nodes have all forced inclusion txs
989990
for i, txHash := range forcedTxHashes {

types/pb/evnode/v1/da.pb.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)