Skip to content

Commit b8a4969

Browse files
committed
Merge branch 'main' into julien/fi
2 parents eb5144c + 31859a7 commit b8a4969

33 files changed

Lines changed: 2410 additions & 184 deletions

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/common/broadcaster_mock.go

Lines changed: 28 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

block/internal/common/expected_interfaces.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ package common
22

33
import (
44
"context"
5+
pubsub "github.com/libp2p/go-libp2p-pubsub"
56

67
"github.com/celestiaorg/go-header"
78
)
89

910
// broadcaster interface for P2P broadcasting
1011
type Broadcaster[H header.Header[H]] interface {
11-
WriteToStoreAndBroadcast(ctx context.Context, payload H) error
12+
WriteToStoreAndBroadcast(ctx context.Context, payload H, opts ...pubsub.PubOpt) error
1213
Store() header.Store[H]
1314
}

block/internal/executing/executor.go

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -654,35 +654,7 @@ func (e *Executor) validateBlock(lastState types.State, header *types.SignedHead
654654
return fmt.Errorf("invalid header: %w", err)
655655
}
656656

657-
// Validate header against data
658-
if err := types.Validate(header, data); err != nil {
659-
return fmt.Errorf("header-data validation failed: %w", err)
660-
}
661-
662-
// Check chain ID
663-
if header.ChainID() != lastState.ChainID {
664-
return fmt.Errorf("chain ID mismatch: expected %s, got %s",
665-
lastState.ChainID, header.ChainID())
666-
}
667-
668-
// Check height
669-
expectedHeight := lastState.LastBlockHeight + 1
670-
if header.Height() != expectedHeight {
671-
return fmt.Errorf("invalid height: expected %d, got %d",
672-
expectedHeight, header.Height())
673-
}
674-
675-
// Check timestamp
676-
if header.Height() > 1 && lastState.LastBlockTime.After(header.Time()) {
677-
return fmt.Errorf("block time must be strictly increasing")
678-
}
679-
680-
// Check app hash
681-
if !bytes.Equal(header.AppHash, lastState.AppHash) {
682-
return fmt.Errorf("app hash mismatch")
683-
}
684-
685-
return nil
657+
return lastState.AssertValidForNextState(header, data)
686658
}
687659

688660
// 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: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@ import (
55
"context"
66
"errors"
77
"fmt"
8+
pubsub "github.com/libp2p/go-libp2p-pubsub"
89
"sync"
910
"sync/atomic"
1011
"time"
1112

1213
"github.com/rs/zerolog"
1314
"golang.org/x/sync/errgroup"
1415

15-
"github.com/evstack/ev-node/block/internal/cache"
16-
"github.com/evstack/ev-node/block/internal/common"
1716
coreda "github.com/evstack/ev-node/core/da"
1817
coreexecutor "github.com/evstack/ev-node/core/execution"
18+
19+
"github.com/evstack/ev-node/block/internal/cache"
20+
"github.com/evstack/ev-node/block/internal/common"
1921
"github.com/evstack/ev-node/pkg/config"
2022
"github.com/evstack/ev-node/pkg/genesis"
2123
"github.com/evstack/ev-node/pkg/store"
@@ -159,6 +161,7 @@ func (s *Syncer) GetLastState() types.State {
159161

160162
stateCopy := *state
161163
stateCopy.AppHash = bytes.Clone(state.AppHash)
164+
stateCopy.LastHeaderHash = bytes.Clone(state.LastHeaderHash)
162165

163166
return stateCopy
164167
}
@@ -183,21 +186,34 @@ func (s *Syncer) initializeState() error {
183186
// Load state from store
184187
state, err := s.store.GetState(s.ctx)
185188
if err != nil {
186-
// Use genesis state if no state exists
189+
// Initialize new chain state for a fresh full node (no prior state on disk)
190+
// Mirror executor initialization to ensure AppHash matches headers produced by the sequencer.
191+
stateRoot, _, initErr := s.exec.InitChain(
192+
s.ctx,
193+
s.genesis.StartTime,
194+
s.genesis.InitialHeight,
195+
s.genesis.ChainID,
196+
)
197+
if initErr != nil {
198+
return fmt.Errorf("failed to initialize execution client: %w", initErr)
199+
}
200+
187201
state = types.State{
188202
ChainID: s.genesis.ChainID,
189203
InitialHeight: s.genesis.InitialHeight,
190204
LastBlockHeight: s.genesis.InitialHeight - 1,
191205
LastBlockTime: s.genesis.StartTime,
192-
DAHeight: 0,
206+
DAHeight: s.genesis.DAStartHeight,
207+
AppHash: stateRoot,
193208
}
194209
}
195-
210+
if state.DAHeight < s.genesis.DAStartHeight {
211+
return fmt.Errorf("DA height (%d) is lower than DA start height (%d)", state.DAHeight, s.genesis.DAStartHeight)
212+
}
196213
s.SetLastState(state)
197214

198215
// Set DA height
199-
daHeight := max(state.DAHeight, s.genesis.DAStartHeight)
200-
s.SetDAHeight(daHeight)
216+
s.SetDAHeight(state.DAHeight)
201217

202218
s.logger.Info().
203219
Uint64("height", state.LastBlockHeight).
@@ -336,6 +352,8 @@ func (s *Syncer) tryFetchFromP2P() {
336352

337353
// Process data (if not already processed by headers)
338354
newDataHeight := s.dataStore.Store().Height()
355+
// TODO @MARKO: why only if newDataHeight != newHeaderHeight? why not process
356+
// just if newDataHeight > currentHeight ?
339357
if newDataHeight != newHeaderHeight && newDataHeight > currentHeight {
340358
s.p2pHandler.ProcessDataRange(s.ctx, currentHeight+1, newDataHeight, s.heightInCh)
341359
}
@@ -386,7 +404,14 @@ func (s *Syncer) processHeightEvent(event *common.DAHeightEvent) {
386404
if err := s.trySyncNextBlock(event); err != nil {
387405
s.logger.Error().Err(err).Msg("failed to sync next block")
388406
// If the error is not due to an validation error, re-store the event as pending
389-
if !errors.Is(err, errInvalidBlock) {
407+
switch {
408+
case errors.Is(err, errInvalidBlock):
409+
// do not reschedule
410+
case errors.Is(err, errInvalidState):
411+
s.sendCriticalError(fmt.Errorf("invalid state detected (block-height %d, state-height %d) "+
412+
"- block references do not match local state. Manual intervention required: %w", event.Header.Height(),
413+
s.GetLastState().LastBlockHeight, err))
414+
default:
390415
s.cache.SetPendingEvent(height, event)
391416
}
392417
return
@@ -395,16 +420,28 @@ func (s *Syncer) processHeightEvent(event *common.DAHeightEvent) {
395420
// only save to p2p stores if the event came from DA
396421
if event.Source == common.SourceDA {
397422
g, ctx := errgroup.WithContext(s.ctx)
398-
g.Go(func() error { return s.headerStore.WriteToStoreAndBroadcast(ctx, event.Header) })
399-
g.Go(func() error { return s.dataStore.WriteToStoreAndBroadcast(ctx, event.Data) })
423+
g.Go(func() error {
424+
// broadcast header locally only — prevents spamming the p2p network with old height notifications,
425+
// allowing the syncer to update its target and fill missing blocks
426+
return s.headerStore.WriteToStoreAndBroadcast(ctx, event.Header, pubsub.WithLocalPublication(true))
427+
})
428+
g.Go(func() error {
429+
// broadcast data locally only — prevents spamming the p2p network with old height notifications,
430+
// allowing the syncer to update its target and fill missing blocks
431+
return s.dataStore.WriteToStoreAndBroadcast(ctx, event.Data, pubsub.WithLocalPublication(true))
432+
})
400433
if err := g.Wait(); err != nil {
401434
s.logger.Error().Err(err).Msg("failed to append event header and/or data to p2p store")
402435
}
403436
}
404437
}
405438

406-
// errInvalidBlock is returned when a block is failing validation
407-
var errInvalidBlock = errors.New("invalid block")
439+
var (
440+
// errInvalidBlock is returned when a block is failing validation
441+
errInvalidBlock = errors.New("invalid block")
442+
// errInvalidState is returned when the state has diverged from the DA blocks
443+
errInvalidState = errors.New("invalid state")
444+
)
408445

409446
// trySyncNextBlock attempts to sync the next available block
410447
// the event is always the next block in sequence as processHeightEvent ensures it.
@@ -426,10 +463,13 @@ func (s *Syncer) trySyncNextBlock(event *common.DAHeightEvent) error {
426463
// Compared to the executor logic where the current block needs to be applied first,
427464
// here only the previous block needs to be applied to proceed to the verification.
428465
// The header validation must be done before applying the block to avoid executing gibberish
429-
if err := s.validateBlock(header, data); err != nil {
466+
if err := s.validateBlock(currentState, data, header); err != nil {
430467
// remove header as da included (not per se needed, but keep cache clean)
431468
s.cache.RemoveHeaderDAIncluded(headerHash)
432-
return errors.Join(errInvalidBlock, fmt.Errorf("failed to validate block: %w", err))
469+
if !errors.Is(err, errInvalidState) && !errors.Is(err, errInvalidBlock) {
470+
return errors.Join(errInvalidBlock, err)
471+
}
472+
return err
433473
}
434474

435475
// Apply block
@@ -535,23 +575,17 @@ func (s *Syncer) executeTxsWithRetry(ctx context.Context, rawTxs [][]byte, heade
535575
// NOTE: if the header was gibberish and somehow passed all validation prior but the data was correct
536576
// or if the data was gibberish and somehow passed all validation prior but the header was correct
537577
// we are still losing both in the pending event. This should never happen.
538-
func (s *Syncer) validateBlock(
539-
header *types.SignedHeader,
540-
data *types.Data,
541-
) error {
578+
func (s *Syncer) validateBlock(currState types.State, data *types.Data, header *types.SignedHeader) error {
542579
// Set custom verifier for aggregator node signature
543580
header.SetCustomVerifierForSyncNode(s.options.SyncNodeSignatureBytesProvider)
544581

545-
// Validate header with data
546582
if err := header.ValidateBasicWithData(data); err != nil {
547583
return fmt.Errorf("invalid header: %w", err)
548584
}
549585

550-
// Validate header against data
551-
if err := types.Validate(header, data); err != nil {
552-
return fmt.Errorf("header-data validation failed: %w", err)
586+
if err := currState.AssertValidForNextState(header, data); err != nil {
587+
return errors.Join(errInvalidState, err)
553588
}
554-
555589
return nil
556590
}
557591

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,

0 commit comments

Comments
 (0)