Skip to content

Commit 70d9fb8

Browse files
committed
Init chain on empty state; Forward runtime errors
1 parent 11fbbee commit 70d9fb8

9 files changed

Lines changed: 152 additions & 79 deletions

File tree

block/components.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ func (bc *Components) GetLastState() types.State {
4646
return types.State{}
4747
}
4848

49-
// Start starts all components and monitors for critical errors
49+
// Start starts all components and monitors for critical errors.
50+
// It is blocking and returns when the context is cancelled or an error occurs
5051
func (bc *Components) Start(ctx context.Context) error {
5152
ctxWithCancel, cancel := context.WithCancel(ctx)
5253

block/internal/executing/executor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ func (e *Executor) initializeState() error {
199199
LastBlockHeight: e.genesis.InitialHeight - 1,
200200
LastBlockTime: e.genesis.StartTime,
201201
AppHash: stateRoot,
202-
DAHeight: 0,
202+
DAHeight: e.genesis.DAStartHeight,
203203
}
204204
}
205205

block/internal/syncing/syncer.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,21 +183,34 @@ func (s *Syncer) initializeState() error {
183183
// Load state from store
184184
state, err := s.store.GetState(s.ctx)
185185
if err != nil {
186-
// Use genesis state if no state exists
186+
// Initialize new chain state for a fresh full node (no prior state on disk)
187+
// Mirror executor initialization to ensure AppHash matches headers produced by the sequencer.
188+
stateRoot, _, initErr := s.exec.InitChain(
189+
s.ctx,
190+
s.genesis.StartTime,
191+
s.genesis.InitialHeight,
192+
s.genesis.ChainID,
193+
)
194+
if initErr != nil {
195+
return fmt.Errorf("failed to initialize execution client: %w", initErr)
196+
}
197+
187198
state = types.State{
188199
ChainID: s.genesis.ChainID,
189200
InitialHeight: s.genesis.InitialHeight,
190201
LastBlockHeight: s.genesis.InitialHeight - 1,
191202
LastBlockTime: s.genesis.StartTime,
192-
DAHeight: 0,
203+
DAHeight: s.genesis.DAStartHeight,
204+
AppHash: stateRoot,
193205
}
194206
}
195-
207+
if state.DAHeight < s.genesis.DAStartHeight {
208+
return fmt.Errorf("DA height (%d) is lower than DA start height (%d)", state.DAHeight, s.genesis.DAStartHeight)
209+
}
196210
s.SetLastState(state)
197211

198212
// Set DA height
199-
daHeight := max(state.DAHeight, s.genesis.DAStartHeight)
200-
s.SetDAHeight(daHeight)
213+
s.SetDAHeight(state.DAHeight)
201214

202215
s.logger.Info().
203216
Uint64("height", state.LastBlockHeight).
@@ -442,6 +455,7 @@ func (s *Syncer) trySyncNextBlock(event *common.DAHeightEvent) error {
442455
if !errors.Is(err, errInvalidState) && !errors.Is(err, errInvalidBlock) {
443456
return errors.Join(errInvalidBlock, err)
444457
}
458+
return err
445459
}
446460

447461
// Apply block

block/internal/syncing/syncer_test.go

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func TestSyncer_validateBlock_DataHashMismatch(t *testing.T) {
111111
cfg := config.DefaultConfig()
112112
gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr}
113113
mockExec := testmocks.NewMockExecutor(t)
114+
mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), uint64(1024), nil).Once()
114115

115116
s := NewSyncer(
116117
st,
@@ -159,7 +160,9 @@ func TestProcessHeightEvent_SyncsAndUpdatesState(t *testing.T) {
159160
gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr}
160161

161162
mockExec := testmocks.NewMockExecutor(t)
163+
mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), uint64(1024), nil).Once()
162164

165+
errChan := make(chan error, 1)
163166
s := NewSyncer(
164167
st,
165168
mockExec,
@@ -172,7 +175,7 @@ func TestProcessHeightEvent_SyncsAndUpdatesState(t *testing.T) {
172175
common.NewMockBroadcaster[*types.Data](t),
173176
zerolog.Nop(),
174177
common.DefaultBlockOptions(),
175-
make(chan error, 1),
178+
errChan,
176179
)
177180

178181
require.NoError(t, s.initializeState())
@@ -190,6 +193,7 @@ func TestProcessHeightEvent_SyncsAndUpdatesState(t *testing.T) {
190193
evt := common.DAHeightEvent{Header: hdr, Data: data, DaHeight: 1}
191194
s.processHeightEvent(&evt)
192195

196+
requireEmptyChan(t, errChan)
193197
h, err := st.Height(context.Background())
194198
require.NoError(t, err)
195199
assert.Equal(t, uint64(1), h)
@@ -209,7 +213,9 @@ func TestSequentialBlockSync(t *testing.T) {
209213
gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr}
210214

211215
mockExec := testmocks.NewMockExecutor(t)
216+
mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), uint64(1024), nil).Once()
212217

218+
errChan := make(chan error, 1)
213219
s := NewSyncer(
214220
st,
215221
mockExec,
@@ -222,7 +228,7 @@ func TestSequentialBlockSync(t *testing.T) {
222228
common.NewMockBroadcaster[*types.Data](t),
223229
zerolog.Nop(),
224230
common.DefaultBlockOptions(),
225-
make(chan error, 1),
231+
errChan,
226232
)
227233
require.NoError(t, s.initializeState())
228234
s.ctx = context.Background()
@@ -265,6 +271,7 @@ func TestSequentialBlockSync(t *testing.T) {
265271
assert.True(t, ok)
266272
_, ok = cm.GetDataDAIncluded(data2.DACommitment().String())
267273
assert.True(t, ok)
274+
requireEmptyChan(t, errChan)
268275
}
269276

270277
func TestSyncer_sendNonBlockingSignal(t *testing.T) {
@@ -328,14 +335,16 @@ func TestSyncer_processPendingEvents(t *testing.T) {
328335
func TestSyncLoopPersistState(t *testing.T) {
329336
ds := dssync.MutexWrap(datastore.NewMapDatastore())
330337
st := store.New(ds)
331-
cm, err := cache.NewManager(config.DefaultConfig(), st, zerolog.Nop())
338+
cfg := config.DefaultConfig()
339+
cfg.ClearCache = true
340+
341+
cacheMgr, err := cache.NewManager(cfg, st, zerolog.Nop())
332342
require.NoError(t, err)
333343

334-
myDAHeightOffset := uint64(1)
335-
myFutureDAHeight := uint64(9)
344+
const myDAHeightOffset = uint64(1)
345+
const numBlocks = uint64(5)
336346

337347
addr, pub, signer := buildSyncTestSigner(t)
338-
cfg := config.DefaultConfig()
339348
gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr, DAStartHeight: myDAHeightOffset}
340349

341350
dummyExec := execution.NewDummyExecutor()
@@ -353,19 +362,20 @@ func TestSyncLoopPersistState(t *testing.T) {
353362
mockP2PDataStore := common.NewMockBroadcaster[*types.Data](t)
354363
mockP2PDataStore.EXPECT().Store().Return(mockDataStore).Maybe()
355364

365+
errorCh := make(chan error, 1)
356366
syncerInst1 := NewSyncer(
357367
st,
358368
dummyExec,
359369
nil,
360-
cm,
370+
cacheMgr,
361371
common.NopMetrics(),
362372
cfg,
363373
gen,
364374
mockP2PHeaderStore,
365375
mockP2PDataStore,
366376
zerolog.Nop(),
367377
common.DefaultBlockOptions(),
368-
make(chan error, 1),
378+
errorCh,
369379
)
370380
require.NoError(t, syncerInst1.initializeState())
371381

@@ -378,13 +388,15 @@ func TestSyncLoopPersistState(t *testing.T) {
378388

379389
// with n da blobs fetched
380390
var prevHeaderHash, prevAppHash []byte
381-
for i := range myFutureDAHeight - myDAHeightOffset {
382-
chainHeight, daHeight := i+1, i+myDAHeightOffset
391+
prevAppHash, _, _ = execution.NewDummyExecutor().InitChain(t.Context(), gen.StartTime, gen.DAStartHeight, gen.ChainID)
392+
for i := range numBlocks {
393+
chainHeight, daHeight := gen.InitialHeight+i, i+myDAHeightOffset
394+
blockTime := gen.StartTime.Add(time.Duration(chainHeight+1) * time.Second)
383395
emptyData := &types.Data{
384396
Metadata: &types.Metadata{
385397
ChainID: gen.ChainID,
386398
Height: chainHeight,
387-
Time: uint64(time.Now().Add(time.Duration(chainHeight) * time.Second).UnixNano()),
399+
Time: uint64(blockTime.UnixNano()),
388400
},
389401
}
390402
_, sigHeader := makeSignedHeaderBytes(t, gen.ChainID, chainHeight, addr, pub, signer, prevAppHash, emptyData, prevHeaderHash)
@@ -401,6 +413,7 @@ func TestSyncLoopPersistState(t *testing.T) {
401413
}
402414

403415
// stop at next height
416+
myFutureDAHeight := myDAHeightOffset + numBlocks
404417
daRtrMock.On("RetrieveFromDA", mock.Anything, myFutureDAHeight).
405418
Run(func(_ mock.Arguments) {
406419
// wait for consumer to catch up
@@ -412,35 +425,39 @@ func TestSyncLoopPersistState(t *testing.T) {
412425
Return(nil, coreda.ErrHeightFromFuture)
413426

414427
go syncerInst1.processLoop()
415-
// dssync from DA until stop height reached
428+
// sync from DA until stop height reached
416429
syncerInst1.syncLoop()
430+
requireEmptyChan(t, errorCh)
431+
417432
t.Log("syncLoop on instance1 completed")
433+
require.Equal(t, myFutureDAHeight, syncerInst1.GetDAHeight())
434+
lastStateDAHeight := syncerInst1.GetLastState().DAHeight
418435

419436
// wait for all events consumed
420-
require.NoError(t, cm.SaveToDisk())
437+
require.NoError(t, cacheMgr.SaveToDisk())
421438
t.Log("processLoop on instance1 completed")
422439

423440
// then
424441
daRtrMock.AssertExpectations(t)
425442
p2pHndlMock.AssertExpectations(t)
443+
require.Len(t, syncerInst1.heightInCh, 0)
426444

427445
// and all processed - verify no events remain at heights we tested
428-
event1 := syncerInst1.cache.GetNextPendingEvent(1)
429-
assert.Nil(t, event1)
430-
event2 := syncerInst1.cache.GetNextPendingEvent(2)
431-
assert.Nil(t, event2)
432-
assert.Len(t, syncerInst1.heightInCh, 0)
433-
446+
for i := range numBlocks {
447+
blockHeight := gen.InitialHeight + i + 1
448+
event := syncerInst1.cache.GetNextPendingEvent(blockHeight)
449+
require.Nil(t, event, "event at height %d should have been removed", blockHeight)
450+
}
434451
// and when new instance is up on restart
435-
cm, err = cache.NewManager(config.DefaultConfig(), st, zerolog.Nop())
452+
cacheMgr, err = cache.NewManager(cfg, st, zerolog.Nop())
436453
require.NoError(t, err)
437-
require.NoError(t, cm.LoadFromDisk())
454+
require.NoError(t, cacheMgr.LoadFromDisk())
438455

439456
syncerInst2 := NewSyncer(
440457
st,
441458
dummyExec,
442459
nil,
443-
cm,
460+
cacheMgr,
444461
common.NopMetrics(),
445462
cfg,
446463
gen,
@@ -451,7 +468,7 @@ func TestSyncLoopPersistState(t *testing.T) {
451468
make(chan error, 1),
452469
)
453470
require.NoError(t, syncerInst2.initializeState())
454-
require.Equal(t, myFutureDAHeight-1, syncerInst2.GetDAHeight())
471+
require.Equal(t, lastStateDAHeight, syncerInst2.GetDAHeight())
455472

456473
ctx, cancel = context.WithCancel(t.Context())
457474
t.Cleanup(cancel)
@@ -465,7 +482,7 @@ func TestSyncLoopPersistState(t *testing.T) {
465482
Run(func(arg mock.Arguments) {
466483
cancel()
467484
// retrieve last one again
468-
assert.Equal(t, myFutureDAHeight-1, arg.Get(1).(uint64))
485+
assert.Equal(t, lastStateDAHeight, arg.Get(1).(uint64))
469486
}).
470487
Return(nil, nil)
471488

@@ -622,3 +639,12 @@ func TestSyncer_InitializeState_CallsReplayer(t *testing.T) {
622639
// Verify that GetLatestHeight was called (proves Replayer was invoked)
623640
mockExec.AssertCalled(t, "GetLatestHeight", mock.Anything)
624641
}
642+
643+
func requireEmptyChan(t *testing.T, errorCh chan error) {
644+
t.Helper()
645+
select {
646+
case err := <-errorCh:
647+
t.Fatalf("syncLoop on instance1 failed: %v", err)
648+
default:
649+
}
650+
}

0 commit comments

Comments
 (0)