Skip to content

Commit 1fce716

Browse files
alpetac0turtle
andauthored
fix: Validate block headers against state (#2763)
This PR got bigger than expected when debugging test failures. The PR includes * Call `InitChain` by Syncer intial state setup as Executor does * Moved new state validation to `state.AssertValidForNextState` with additional checks to ensure consistency. Used by Syncer and Executor * Abort execution on invalid state errors * Return runtime errors in fullnode run for better error tracing --------- Co-authored-by: Marko <[email protected]>
1 parent da374bf commit 1fce716

23 files changed

+538
-169
lines changed

block/components.go

Lines changed: 4 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

@@ -137,6 +138,7 @@ func NewSyncComponents(
137138
metrics *Metrics,
138139
blockOpts BlockOptions,
139140
) (*Components, error) {
141+
logger.Info().Msg("Starting in sync-mode")
140142
cacheManager, err := cache.NewManager(config, store, logger)
141143
if err != nil {
142144
return nil, fmt.Errorf("failed to create cache manager: %w", err)
@@ -200,6 +202,7 @@ func NewAggregatorComponents(
200202
metrics *Metrics,
201203
blockOpts BlockOptions,
202204
) (*Components, error) {
205+
logger.Info().Msg("Starting in aggregator-mode")
203206
cacheManager, err := cache.NewManager(config, store, logger)
204207
if err != nil {
205208
return nil, fmt.Errorf("failed to create cache manager: %w", err)

block/internal/executing/executor.go

Lines changed: 2 additions & 30 deletions
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

@@ -633,35 +633,7 @@ func (e *Executor) validateBlock(lastState types.State, header *types.SignedHead
633633
return fmt.Errorf("invalid header: %w", err)
634634
}
635635

636-
// Validate header against data
637-
if err := types.Validate(header, data); err != nil {
638-
return fmt.Errorf("header-data validation failed: %w", err)
639-
}
640-
641-
// Check chain ID
642-
if header.ChainID() != lastState.ChainID {
643-
return fmt.Errorf("chain ID mismatch: expected %s, got %s",
644-
lastState.ChainID, header.ChainID())
645-
}
646-
647-
// Check height
648-
expectedHeight := lastState.LastBlockHeight + 1
649-
if header.Height() != expectedHeight {
650-
return fmt.Errorf("invalid height: expected %d, got %d",
651-
expectedHeight, header.Height())
652-
}
653-
654-
// Check timestamp
655-
if header.Height() > 1 && lastState.LastBlockTime.After(header.Time()) {
656-
return fmt.Errorf("block time must be strictly increasing")
657-
}
658-
659-
// Check app hash
660-
if !bytes.Equal(header.AppHash, lastState.AppHash) {
661-
return fmt.Errorf("app hash mismatch")
662-
}
663-
664-
return nil
636+
return lastState.AssertValidForNextState(header, data)
665637
}
666638

667639
// sendCriticalError sends a critical error to the error channel without blocking

block/internal/syncing/da_retriever_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ func TestDARetriever_ProcessBlobs_HeaderAndData_Success(t *testing.T) {
168168
r := NewDARetriever(nil, cm, config.DefaultConfig(), gen, zerolog.Nop())
169169

170170
dataBin, data := makeSignedDataBytes(t, gen.ChainID, 2, addr, pub, signer, 2)
171-
hdrBin, _ := makeSignedHeaderBytes(t, gen.ChainID, 2, addr, pub, signer, nil, &data.Data)
171+
hdrBin, _ := makeSignedHeaderBytes(t, gen.ChainID, 2, addr, pub, signer, nil, &data.Data, nil)
172172

173173
events := r.processBlobs(context.Background(), [][]byte{hdrBin, dataBin}, 77)
174174
require.Len(t, events, 1)
@@ -196,7 +196,7 @@ func TestDARetriever_ProcessBlobs_HeaderOnly_EmptyDataExpected(t *testing.T) {
196196
r := NewDARetriever(nil, cm, config.DefaultConfig(), gen, zerolog.Nop())
197197

198198
// Header with no data hash present should trigger empty data creation (per current logic)
199-
hb, _ := makeSignedHeaderBytes(t, gen.ChainID, 3, addr, pub, signer, nil, nil)
199+
hb, _ := makeSignedHeaderBytes(t, gen.ChainID, 3, addr, pub, signer, nil, nil, nil)
200200

201201
events := r.processBlobs(context.Background(), [][]byte{hb}, 88)
202202
require.Len(t, events, 1)
@@ -223,7 +223,7 @@ func TestDARetriever_TryDecodeHeaderAndData_Basic(t *testing.T) {
223223
gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr}
224224
r := NewDARetriever(nil, cm, config.DefaultConfig(), gen, zerolog.Nop())
225225

226-
hb, sh := makeSignedHeaderBytes(t, gen.ChainID, 5, addr, pub, signer, nil, nil)
226+
hb, sh := makeSignedHeaderBytes(t, gen.ChainID, 5, addr, pub, signer, nil, nil, nil)
227227
gotH := r.tryDecodeHeader(hb, 123)
228228
require.NotNil(t, gotH)
229229
assert.Equal(t, sh.Hash().String(), gotH.Hash().String())
@@ -279,7 +279,7 @@ func TestDARetriever_RetrieveFromDA_TwoNamespaces_Success(t *testing.T) {
279279

280280
// Prepare header/data blobs
281281
dataBin, data := makeSignedDataBytes(t, gen.ChainID, 9, addr, pub, signer, 1)
282-
hdrBin, _ := makeSignedHeaderBytes(t, gen.ChainID, 9, addr, pub, signer, nil, &data.Data)
282+
hdrBin, _ := makeSignedHeaderBytes(t, gen.ChainID, 9, addr, pub, signer, nil, &data.Data, nil)
283283

284284
cfg := config.DefaultConfig()
285285
cfg.DA.Namespace = "nsHdr"
@@ -322,7 +322,7 @@ func TestDARetriever_ProcessBlobs_CrossDAHeightMatching(t *testing.T) {
322322

323323
// Create header and data for the same block height but from different DA heights
324324
dataBin, data := makeSignedDataBytes(t, gen.ChainID, 5, addr, pub, signer, 2)
325-
hdrBin, _ := makeSignedHeaderBytes(t, gen.ChainID, 5, addr, pub, signer, nil, &data.Data)
325+
hdrBin, _ := makeSignedHeaderBytes(t, gen.ChainID, 5, addr, pub, signer, nil, &data.Data, nil)
326326

327327
// Process header from DA height 100 first
328328
events1 := r.processBlobs(context.Background(), [][]byte{hdrBin}, 100)
@@ -361,9 +361,9 @@ func TestDARetriever_ProcessBlobs_MultipleHeadersCrossDAHeightMatching(t *testin
361361
data4Bin, data4 := makeSignedDataBytes(t, gen.ChainID, 4, addr, pub, signer, 2)
362362
data5Bin, data5 := makeSignedDataBytes(t, gen.ChainID, 5, addr, pub, signer, 1)
363363

364-
hdr3Bin, _ := makeSignedHeaderBytes(t, gen.ChainID, 3, addr, pub, signer, nil, &data3.Data)
365-
hdr4Bin, _ := makeSignedHeaderBytes(t, gen.ChainID, 4, addr, pub, signer, nil, &data4.Data)
366-
hdr5Bin, _ := makeSignedHeaderBytes(t, gen.ChainID, 5, addr, pub, signer, nil, &data5.Data)
364+
hdr3Bin, _ := makeSignedHeaderBytes(t, gen.ChainID, 3, addr, pub, signer, nil, &data3.Data, nil)
365+
hdr4Bin, _ := makeSignedHeaderBytes(t, gen.ChainID, 4, addr, pub, signer, nil, &data4.Data, nil)
366+
hdr5Bin, _ := makeSignedHeaderBytes(t, gen.ChainID, 5, addr, pub, signer, nil, &data5.Data, nil)
367367

368368
// Process multiple headers from DA height 200 - should be stored as pending
369369
events1 := r.processBlobs(context.Background(), [][]byte{hdr3Bin, hdr4Bin, hdr5Bin}, 200)

block/internal/syncing/syncer.go

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ func (s *Syncer) GetLastState() types.State {
158158

159159
stateCopy := *state
160160
stateCopy.AppHash = bytes.Clone(state.AppHash)
161+
stateCopy.LastHeaderHash = bytes.Clone(state.LastHeaderHash)
161162

162163
return stateCopy
163164
}
@@ -182,21 +183,34 @@ func (s *Syncer) initializeState() error {
182183
// Load state from store
183184
state, err := s.store.GetState(s.ctx)
184185
if err != nil {
185-
// 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+
186198
state = types.State{
187199
ChainID: s.genesis.ChainID,
188200
InitialHeight: s.genesis.InitialHeight,
189201
LastBlockHeight: s.genesis.InitialHeight - 1,
190202
LastBlockTime: s.genesis.StartTime,
191-
DAHeight: 0,
203+
DAHeight: s.genesis.DAStartHeight,
204+
AppHash: stateRoot,
192205
}
193206
}
194-
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+
}
195210
s.SetLastState(state)
196211

197212
// Set DA height
198-
daHeight := max(state.DAHeight, s.genesis.DAStartHeight)
199-
s.SetDAHeight(daHeight)
213+
s.SetDAHeight(state.DAHeight)
200214

201215
s.logger.Info().
202216
Uint64("height", state.LastBlockHeight).
@@ -385,7 +399,14 @@ func (s *Syncer) processHeightEvent(event *common.DAHeightEvent) {
385399
if err := s.trySyncNextBlock(event); err != nil {
386400
s.logger.Error().Err(err).Msg("failed to sync next block")
387401
// If the error is not due to an validation error, re-store the event as pending
388-
if !errors.Is(err, errInvalidBlock) {
402+
switch {
403+
case errors.Is(err, errInvalidBlock):
404+
// do not reschedule
405+
case errors.Is(err, errInvalidState):
406+
s.sendCriticalError(fmt.Errorf("invalid state detected (block-height %d, state-height %d) "+
407+
"- block references do not match local state. Manual intervention required: %w", event.Header.Height(),
408+
s.GetLastState().LastBlockHeight, err))
409+
default:
389410
s.cache.SetPendingEvent(height, event)
390411
}
391412
return
@@ -402,8 +423,12 @@ func (s *Syncer) processHeightEvent(event *common.DAHeightEvent) {
402423
}
403424
}
404425

405-
// errInvalidBlock is returned when a block is failing validation
406-
var errInvalidBlock = errors.New("invalid block")
426+
var (
427+
// errInvalidBlock is returned when a block is failing validation
428+
errInvalidBlock = errors.New("invalid block")
429+
// errInvalidState is returned when the state has diverged from the DA blocks
430+
errInvalidState = errors.New("invalid state")
431+
)
407432

408433
// trySyncNextBlock attempts to sync the next available block
409434
// the event is always the next block in sequence as processHeightEvent ensures it.
@@ -425,10 +450,13 @@ func (s *Syncer) trySyncNextBlock(event *common.DAHeightEvent) error {
425450
// Compared to the executor logic where the current block needs to be applied first,
426451
// here only the previous block needs to be applied to proceed to the verification.
427452
// The header validation must be done before applying the block to avoid executing gibberish
428-
if err := s.validateBlock(header, data); err != nil {
453+
if err := s.validateBlock(currentState, data, header); err != nil {
429454
// remove header as da included (not per se needed, but keep cache clean)
430455
s.cache.RemoveHeaderDAIncluded(headerHash)
431-
return errors.Join(errInvalidBlock, fmt.Errorf("failed to validate block: %w", err))
456+
if !errors.Is(err, errInvalidState) && !errors.Is(err, errInvalidBlock) {
457+
return errors.Join(errInvalidBlock, err)
458+
}
459+
return err
432460
}
433461

434462
// Apply block
@@ -534,23 +562,17 @@ func (s *Syncer) executeTxsWithRetry(ctx context.Context, rawTxs [][]byte, heade
534562
// NOTE: if the header was gibberish and somehow passed all validation prior but the data was correct
535563
// or if the data was gibberish and somehow passed all validation prior but the header was correct
536564
// we are still losing both in the pending event. This should never happen.
537-
func (s *Syncer) validateBlock(
538-
header *types.SignedHeader,
539-
data *types.Data,
540-
) error {
565+
func (s *Syncer) validateBlock(currState types.State, data *types.Data, header *types.SignedHeader) error {
541566
// Set custom verifier for aggregator node signature
542567
header.SetCustomVerifierForSyncNode(s.options.SyncNodeSignatureBytesProvider)
543568

544-
// Validate header with data
545569
if err := header.ValidateBasicWithData(data); err != nil {
546570
return fmt.Errorf("invalid header: %w", err)
547571
}
548572

549-
// Validate header against data
550-
if err := types.Validate(header, data); err != nil {
551-
return fmt.Errorf("header-data validation failed: %w", err)
573+
if err := currState.AssertValidForNextState(header, data); err != nil {
574+
return errors.Join(errInvalidState, err)
552575
}
553-
554576
return nil
555577
}
556578

block/internal/syncing/syncer_backoff_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ func TestSyncer_BackoffResetOnSuccess(t *testing.T) {
197197
Return(nil, errors.New("temporary failure")).Once()
198198

199199
// Second call - success (should reset backoff and increment DA height)
200-
_, header := makeSignedHeaderBytes(t, gen.ChainID, 1, addr, pub, signer, nil, nil)
200+
_, header := makeSignedHeaderBytes(t, gen.ChainID, 1, addr, pub, signer, nil, nil, nil)
201201
data := &types.Data{
202202
Metadata: &types.Metadata{
203203
ChainID: gen.ChainID,

block/internal/syncing/syncer_benchmark_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func newBenchFixture(b *testing.B, totalHeights uint64, shuffledTx bool, daDelay
120120
heightEvents := make([]common.DAHeightEvent, totalHeights)
121121
for i := uint64(0); i < totalHeights; i++ {
122122
blockHeight, daHeight := i+gen.InitialHeight, i+daHeightOffset
123-
_, sh := makeSignedHeaderBytes(b, gen.ChainID, blockHeight, addr, pub, signer, nil, nil)
123+
_, sh := makeSignedHeaderBytes(b, gen.ChainID, blockHeight, addr, pub, signer, nil, nil, nil)
124124
d := &types.Data{Metadata: &types.Metadata{ChainID: gen.ChainID, Height: blockHeight, Time: uint64(time.Now().UnixNano())}}
125125
heightEvents[i] = common.DAHeightEvent{Header: sh, Data: d, DaHeight: daHeight}
126126
}

0 commit comments

Comments
 (0)