diff --git a/block/DIVERGENCE.md b/block/DIVERGENCE.md new file mode 100644 index 0000000000..5368195240 --- /dev/null +++ b/block/DIVERGENCE.md @@ -0,0 +1,72 @@ +# Divergence from Main Branch + +This branch (`perf/block-optimization`) introduces breaking changes to maximize performance in the `block/` package. It is **not wire-compatible** with the main branch. + +## 1. Combined Header+Data Blobs + +**Main**: Headers and data are submitted as separate blobs to separate DA namespaces (`HeaderNamespace`, `DataNamespace`). On retrieval, the DA retriever fetches from both namespaces, decodes headers and data independently, then matches them by block height. + +**This branch**: Headers and data are combined into a single blob using a custom binary encoding (`common.MarshalBlockBlob`/`UnmarshalBlockBlob`). Each blob contains the proto-encoded header, proto-encoded data, and the envelope signature, separated by length prefixes with a magic number prefix (`0x45564E44`). Only the `HeaderNamespace` is used. + +### Why +- Eliminates matching overhead (no separate header/data pending maps) +- Halves DA submission round trips (one blob per block instead of two) +- Simplifies DA inclusion tracking (single check per block) +- Removes the `DAHeaderEnvelope` protobuf wrapper and the separate `SignedData` protobuf wrapper + +## 2. Custom Binary Blob Encoding + +**Main**: DA blobs use protobuf encoding (`DAHeaderEnvelope` for headers, `SignedData` for data). Each involves allocating proto message structs, converting Go types to proto types, and calling `proto.Marshal`. + +**This branch**: The combined blob wrapper uses a custom binary format: `[magic 4B][header_len 4B][header_bytes][data_len 4B][data_bytes][sig_len 4B][sig_bytes]`. Individual header and data fields are still proto-encoded internally (hash computation requires it), but the envelope wrapper avoids all proto overhead. + +### Why +- Zero allocation for the blob wrapper (direct length-prefixed binary) +- No proto message pool management for the envelope +- No `ToProto`/`FromProto` conversion for the DA envelope or signed data +- Simpler and faster encode/decode path + +## 3. P2P Sync Removed + +**Main**: Full nodes sync from both P2P (via `go-header` `HeaderSyncService`/`DataSyncService`) and DA. The executor broadcasts produced blocks to P2P peers. P2P events include DA height hints for targeted DA retrieval. The syncer runs a P2P worker loop alongside the DA follower. + +**This branch**: All P2P sync is removed. Nodes sync exclusively from DA. No P2P broadcasting, no P2P stores, no P2P handler, no DA height hints. + +### Removed code +- `syncing/p2p_handler.go` — entire file deleted +- `syncing/syncer_mock.go` — P2P handler mock deleted +- `common/expected_interfaces.go` — `HeaderP2PBroadcaster`/`DataP2PBroadcaster` types removed +- P2P broadcasting in `executing/executor.go` removed +- P2P worker loop in syncer removed +- `SourceP2P` event source removed +- `DaHeightHints` field removed from `DAHeightEvent` +- `headerStore`/`dataStore` parameters removed from `NewSyncer` and component constructors +- `headerSyncService`/`dataSyncService` parameters removed from aggregator component constructors +- `DAHintAppender` interface removed from DA submitter +- Separate `SubmitHeaders`/`SubmitData` replaced with single `SubmitBlocks` + +### Why +- Removes network overhead from P2P gossip +- Eliminates the complexity of two sync sources competing +- DA is the single source of truth, reducing consistency issues +- Removes libp2p dependency from the block package's hot path +- Simplifies the syncer from 3 worker loops to 2 (process loop + pending worker) + +## 4. DA Submitter Simplified + +**Main**: `DASubmitterAPI` has two methods: `SubmitHeaders` and `SubmitData`, each with separate batching strategies, mutex locks, and retry loops. + +**This branch**: `DASubmitterAPI` has a single `SubmitBlocks` method that takes headers and data together, creates combined blobs, signs them, and submits to a single namespace. One batching strategy, one mutex, one retry loop. + +### Why +- Halves the submission loop complexity +- Eliminates the envelope cache (no more retry-signing concern) +- Single retry loop instead of two +- Combined blobs submitted atomically — no partial header-without-data states + +## Migration Notes + +- Existing DA data from main branch is **not readable** by this branch (different blob format) +- This branch requires a fresh start or a migration tool +- The `P2PSignedHeader` and `P2PData` types still exist in `types/` but are no longer used by the block package +- External consumers of `NewSyncComponents` and `NewAggregatorWithCatchupComponents` must update their call sites diff --git a/block/components.go b/block/components.go index ac5f782cdd..483a1bec3c 100644 --- a/block/components.go +++ b/block/components.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" - "github.com/celestiaorg/go-header" "github.com/rs/zerolog" "github.com/evstack/ev-node/block/internal/cache" @@ -22,9 +21,7 @@ import ( "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/pkg/signer" "github.com/evstack/ev-node/pkg/store" - "github.com/evstack/ev-node/pkg/sync" "github.com/evstack/ev-node/pkg/telemetry" - "github.com/evstack/ev-node/types" ) // Components represents the block-related components @@ -133,7 +130,7 @@ func (bc *Components) Stop() error { } // NewSyncComponents creates components for a non-aggregator full node that can only sync blocks. -// Non-aggregator full nodes can sync from P2P and DA but cannot produce blocks or submit to DA. +// Non-aggregator full nodes can sync from DA but cannot produce blocks or submit to DA. // They have more sync capabilities than light nodes but no block production. No signer required. func NewSyncComponents( config config.Config, @@ -141,10 +138,6 @@ func NewSyncComponents( store store.Store, exec coreexecutor.Executor, daClient da.Client, - headerStore header.Store[*types.P2PSignedHeader], - dataStore header.Store[*types.P2PData], - headerDAHintAppender submitting.DAHintAppender, - dataDAHintAppender submitting.DAHintAppender, logger zerolog.Logger, metrics *Metrics, blockOpts BlockOptions, @@ -166,8 +159,6 @@ func NewSyncComponents( metrics, config, genesis, - headerStore, - dataStore, logger, blockOpts, errorCh, @@ -182,10 +173,10 @@ func NewSyncComponents( if p, ok := exec.(coreexecutor.ExecPruner); ok { execPruner = p } - pruner := pruner.New(logger, store, execPruner, config.Pruning, config.Node.BlockTime.Duration, config.DA.Address) + prunerObj := pruner.New(logger, store, execPruner, config.Pruning, config.Node.BlockTime.Duration, config.DA.Address) // Create submitter for sync nodes (no signer, only DA inclusion processing) - var daSubmitter submitting.DASubmitterAPI = submitting.NewDASubmitter(daClient, config, genesis, blockOpts, metrics, logger, headerDAHintAppender, dataDAHintAppender) + var daSubmitter submitting.DASubmitterAPI = submitting.NewDASubmitter(daClient, config, genesis, blockOpts, metrics, logger) if config.Instrumentation.IsTracingEnabled() { daSubmitter = submitting.WithTracingDASubmitter(daSubmitter) } @@ -207,13 +198,13 @@ func NewSyncComponents( Syncer: syncer, Submitter: submitter, Cache: cacheManager, - Pruner: pruner, + Pruner: prunerObj, errorCh: errorCh, }, nil } // newAggregatorComponents creates components for an aggregator full node that can produce and sync blocks. -// Aggregator nodes have full capabilities - they can produce blocks, sync from P2P and DA, +// Aggregator nodes have full capabilities - they can produce blocks, sync from DA, // and submit headers/data to DA. Requires a signer for block production and DA submission. func newAggregatorComponents( config config.Config, @@ -223,8 +214,6 @@ func newAggregatorComponents( sequencer coresequencer.Sequencer, daClient da.Client, signer signer.Signer, - headerSyncService *sync.HeaderSyncService, - dataSyncService *sync.DataSyncService, logger zerolog.Logger, metrics *Metrics, blockOpts BlockOptions, @@ -252,8 +241,6 @@ func newAggregatorComponents( metrics, config, genesis, - headerSyncService, - dataSyncService, logger, blockOpts, errorCh, @@ -271,7 +258,7 @@ func newAggregatorComponents( if p, ok := exec.(coreexecutor.ExecPruner); ok { execPruner = p } - pruner := pruner.New(logger, store, execPruner, config.Pruning, config.Node.BlockTime.Duration, config.DA.Address) + prunerObj := pruner.New(logger, store, execPruner, config.Pruning, config.Node.BlockTime.Duration, config.DA.Address) reaper, err := reaping.NewReaper( exec, @@ -286,17 +273,17 @@ func newAggregatorComponents( return nil, fmt.Errorf("failed to create reaper: %w", err) } - if config.Node.BasedSequencer { // no submissions needed for bases sequencer + if config.Node.BasedSequencer { // no submissions needed for based sequencer return &Components{ Executor: executor, - Pruner: pruner, + Pruner: prunerObj, Reaper: reaper, Cache: cacheManager, errorCh: errorCh, }, nil } - var daSubmitter submitting.DASubmitterAPI = submitting.NewDASubmitter(daClient, config, genesis, blockOpts, metrics, logger, headerSyncService, dataSyncService) + var daSubmitter submitting.DASubmitterAPI = submitting.NewDASubmitter(daClient, config, genesis, blockOpts, metrics, logger) if config.Instrumentation.IsTracingEnabled() { daSubmitter = submitting.WithTracingDASubmitter(daSubmitter) } @@ -316,7 +303,7 @@ func newAggregatorComponents( return &Components{ Executor: executor, - Pruner: pruner, + Pruner: prunerObj, Reaper: reaper, Submitter: submitter, Cache: cacheManager, @@ -325,10 +312,10 @@ func newAggregatorComponents( } // NewAggregatorWithCatchupComponents creates aggregator components that include a Syncer -// for DA/P2P catchup before block production begins. +// for DA catchup before block production begins. // // The caller should: -// 1. Start the Syncer and wait for DA head + P2P catchup +// 1. Start the Syncer and wait for DA head catchup // 2. Stop the Syncer and set Components.Syncer = nil // 3. Call Components.Start() — which will start the Executor and other components func NewAggregatorWithCatchupComponents( @@ -339,8 +326,6 @@ func NewAggregatorWithCatchupComponents( sequencer coresequencer.Sequencer, daClient da.Client, signer signer.Signer, - headerSyncService *sync.HeaderSyncService, - dataSyncService *sync.DataSyncService, logger zerolog.Logger, metrics *Metrics, blockOpts BlockOptions, @@ -348,7 +333,7 @@ func NewAggregatorWithCatchupComponents( ) (*Components, error) { bc, err := newAggregatorComponents( config, genesis, store, exec, sequencer, daClient, signer, - headerSyncService, dataSyncService, logger, metrics, blockOpts, raftNode, + logger, metrics, blockOpts, raftNode, ) if err != nil { return nil, err @@ -364,8 +349,6 @@ func NewAggregatorWithCatchupComponents( metrics, config, genesis, - headerSyncService.Store(), - dataSyncService.Store(), logger, blockOpts, catchupErrCh, diff --git a/block/components_test.go b/block/components_test.go index 527b7061cf..36c1c1e633 100644 --- a/block/components_test.go +++ b/block/components_test.go @@ -23,20 +23,10 @@ import ( "github.com/evstack/ev-node/pkg/signer/noop" "github.com/evstack/ev-node/pkg/store" testmocks "github.com/evstack/ev-node/test/mocks" - extmocks "github.com/evstack/ev-node/test/mocks/external" - "github.com/evstack/ev-node/types" ) -// noopDAHintAppender is a no-op implementation of DAHintAppender for testing -type noopDAHintAppender struct{} - -func (n noopDAHintAppender) AppendDAHint(ctx context.Context, daHeight uint64, heights ...uint64) error { - return nil -} - +// Test the error channel mechanism works as intended func TestBlockComponents_ExecutionClientFailure_StopsNode(t *testing.T) { - // Test the error channel mechanism works as intended - // Create a mock component that simulates execution client failure errorCh := make(chan error, 1) criticalError := errors.New("execution client connection lost") @@ -62,13 +52,13 @@ func TestBlockComponents_ExecutionClientFailure_StopsNode(t *testing.T) { assert.Contains(t, err.Error(), "execution client connection lost") } +// Simple lifecycle test without creating full components func TestBlockComponents_StartStop_Lifecycle(t *testing.T) { - // Simple lifecycle test without creating full components + // Test that Start and Stop work without hanging bc := &Components{ errorCh: make(chan error, 1), } - // Test that Start and Stop work without hanging ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() @@ -96,26 +86,12 @@ func TestNewSyncComponents_Creation(t *testing.T) { daClient.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() daClient.On("HasForcedInclusionNamespace").Return(false).Maybe() - // Create mock P2P stores - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - - // Create noop DAHintAppenders for testing - headerHintAppender := noopDAHintAppender{} - dataHintAppender := noopDAHintAppender{} - - // Just test that the constructor doesn't panic - don't start the components - // to avoid P2P store dependencies components, err := NewSyncComponents( cfg, gen, memStore, mockExec, daClient, - mockHeaderStore, - mockDataStore, - headerHintAppender, - dataHintAppender, zerolog.Nop(), NopMetrics(), DefaultBlockOptions(), @@ -171,12 +147,10 @@ func TestNewAggregatorComponents_Creation(t *testing.T) { mockSeq, daClient, mockSigner, - nil, // header broadcaster - nil, // data broadcaster zerolog.Nop(), NopMetrics(), DefaultBlockOptions(), - nil, // raftNode + nil, ) require.NoError(t, err) @@ -189,9 +163,9 @@ func TestNewAggregatorComponents_Creation(t *testing.T) { assert.Nil(t, components.Syncer) // Aggregator nodes currently don't create syncers in this constructor } +// This test verifies that when the executor's execution client calls fail, +// the error is properly propagated through the error channel and stops the node func TestExecutor_RealExecutionClientFailure_StopsNode(t *testing.T) { - // This test verifies that when the executor's execution client calls fail, - // the error is properly propagated through the error channel and stops the node synctest.Test(t, func(t *testing.T) { ds := sync.MutexWrap(datastore.NewMapDatastore()) memStore := store.New(ds) @@ -255,12 +229,10 @@ func TestExecutor_RealExecutionClientFailure_StopsNode(t *testing.T) { mockSeq, daClient, testSigner, - nil, // header broadcaster - nil, // data broadcaster zerolog.Nop(), NopMetrics(), DefaultBlockOptions(), - nil, // raftNode + nil, ) require.NoError(t, err) diff --git a/block/internal/common/blob.go b/block/internal/common/blob.go new file mode 100644 index 0000000000..31e312f0c5 --- /dev/null +++ b/block/internal/common/blob.go @@ -0,0 +1,90 @@ +package common + +import ( + "encoding/binary" + "fmt" + + "github.com/evstack/ev-node/types" +) + +const blobMagic uint32 = 0x45564E44 + +// MarshalBlockBlob encodes a signed header, data and envelope signature into a +// single binary blob using a custom length-prefixed format. +// Layout: [magic 4B][header_len 4B][header][data_len 4B][data][sig_len 4B][sig] +func MarshalBlockBlob(header *types.SignedHeader, data *types.Data, envelopeSig []byte) ([]byte, error) { + headerBz, err := header.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("marshal header: %w", err) + } + dataBz, err := data.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("marshal data: %w", err) + } + + size := 4 + 4 + len(headerBz) + 4 + len(dataBz) + 4 + len(envelopeSig) + buf := make([]byte, 0, size) + + buf = binary.BigEndian.AppendUint32(buf, blobMagic) + buf = binary.BigEndian.AppendUint32(buf, uint32(len(headerBz))) + buf = append(buf, headerBz...) + buf = binary.BigEndian.AppendUint32(buf, uint32(len(dataBz))) + buf = append(buf, dataBz...) + buf = binary.BigEndian.AppendUint32(buf, uint32(len(envelopeSig))) + buf = append(buf, envelopeSig...) + + return buf, nil +} + +// UnmarshalBlockBlob decodes a combined block blob produced by MarshalBlockBlob. +func UnmarshalBlockBlob(bz []byte) (*types.SignedHeader, *types.Data, []byte, error) { + if len(bz) < 4 { + return nil, nil, nil, fmt.Errorf("blob too short: %d", len(bz)) + } + if magic := binary.BigEndian.Uint32(bz[:4]); magic != blobMagic { + return nil, nil, nil, fmt.Errorf("invalid blob magic: %x", magic) + } + off := 4 + + header, off, err := readBlobField(bz, off, "header") + if err != nil { + return nil, nil, nil, err + } + var signedHeader types.SignedHeader + if err := signedHeader.UnmarshalBinary(header); err != nil { + return nil, nil, nil, fmt.Errorf("unmarshal header: %w", err) + } + + dataBz, off, err := readBlobField(bz, off, "data") + if err != nil { + return nil, nil, nil, err + } + var data types.Data + if err := data.UnmarshalBinary(dataBz); err != nil { + return nil, nil, nil, fmt.Errorf("unmarshal data: %w", err) + } + + var envelopeSig []byte + if off+4 <= len(bz) { + sig, _, sigErr := readBlobField(bz, off, "signature") + if sigErr == nil { + envelopeSig = sig + } + } + + return &signedHeader, &data, envelopeSig, nil +} + +func readBlobField(bz []byte, off int, name string) ([]byte, int, error) { + if off+4 > len(bz) { + return nil, off, fmt.Errorf("truncated %s length at offset %d", name, off) + } + fieldLen := int(binary.BigEndian.Uint32(bz[off : off+4])) + off += 4 + if off+fieldLen > len(bz) { + return nil, off, fmt.Errorf("truncated %s data: need %d, have %d", name, fieldLen, len(bz)-off) + } + field := bz[off : off+fieldLen] + off += fieldLen + return field, off, nil +} diff --git a/block/internal/common/consts.go b/block/internal/common/consts.go index 8e1e679fc3..45df5cdfce 100644 --- a/block/internal/common/consts.go +++ b/block/internal/common/consts.go @@ -36,10 +36,10 @@ var defaultMaxBlobSizeStr = "134217723" // 1 << 27 - 5 = 128 MiB - 5 B // using MaxBlobSize as both input cap and output cap let blocks blow // past the DA cap). Split into two: // -// MaxBlobSize — chain-side ceiling on a marshaled DA blob -// MaxBlockTxBytes() — derived raw-tx budget = MaxBlobSize - per-block -// marshal overhead. Used by RetrieveBatch / -// FilterTxs. +// MaxBlobSize — chain-side ceiling on a marshaled DA blob +// MaxBlockTxBytes() — derived raw-tx budget = MaxBlobSize - per-block +// marshal overhead. Used by RetrieveBatch / +// FilterTxs. // // Once that derivation exists, drop the ad-hoc 2% reservation in // executing/executor.go::RetrieveBatch and the duplicate cap in diff --git a/block/internal/common/event.go b/block/internal/common/event.go index af7e267603..1b9a86a005 100644 --- a/block/internal/common/event.go +++ b/block/internal/common/event.go @@ -12,15 +12,13 @@ type EventSource string const ( // SourceDA indicates the event came from the DA layer SourceDA EventSource = "da" - // SourceP2P indicates the event came from P2P network - SourceP2P EventSource = "p2p" // SourceRaft indicates the event came from Raft consensus recovery SourceRaft EventSource = "raft" ) // AllEventSources returns all possible event sources. func AllEventSources() []EventSource { - return []EventSource{SourceDA, SourceP2P, SourceRaft} + return []EventSource{SourceDA, SourceRaft} } // DAHeightEvent represents a DA event for caching @@ -29,11 +27,8 @@ type DAHeightEvent struct { Data *types.Data // DaHeight corresponds to the highest DA included height between the Header and Data. DaHeight uint64 - // Source indicates where this event originated from (DA or P2P) + // Source indicates where this event originated from (DA or Raft) Source EventSource - - // Optional DA height hints from P2P. first is the DA height hint for the header, second is the DA height hint for the data - DaHeightHints [2]uint64 } // EventSink receives parsed DA events with backpressure support. diff --git a/block/internal/common/expected_interfaces.go b/block/internal/common/expected_interfaces.go index 9bc3465642..805d0c79aa 100644 --- a/block/internal/common/expected_interfaces.go +++ b/block/internal/common/expected_interfaces.go @@ -1,22 +1 @@ package common - -import ( - "context" - - "github.com/celestiaorg/go-header" - pubsub "github.com/libp2p/go-libp2p-pubsub" - - "github.com/evstack/ev-node/types" -) - -type ( - HeaderP2PBroadcaster = Broadcaster[*types.P2PSignedHeader] - DataP2PBroadcaster = Broadcaster[*types.P2PData] -) - -// Broadcaster interface for P2P broadcasting -type Broadcaster[H header.Header[H]] interface { - WriteToStoreAndBroadcast(ctx context.Context, payload H, opts ...pubsub.PubOpt) error - Store() header.Store[H] - Height() uint64 -} diff --git a/block/internal/da/fiber_client_test.go b/block/internal/da/fiber_client_test.go index fed6f978f7..847dc16f3a 100644 --- a/block/internal/da/fiber_client_test.go +++ b/block/internal/da/fiber_client_test.go @@ -10,6 +10,7 @@ import ( "github.com/rs/zerolog" "github.com/stretchr/testify/require" + "github.com/evstack/ev-node/block/internal/common" "github.com/evstack/ev-node/block/internal/da/fiber" "github.com/evstack/ev-node/block/internal/da/fibremock" datypes "github.com/evstack/ev-node/pkg/da/types" @@ -126,7 +127,7 @@ func TestFiberClient_Submit_BlobTooLarge(t *testing.T) { _, cl := makeTestFiberClient(t) ns := datypes.NamespaceFromString("test-ns").Bytes() - largeBlob := make([]byte, 6*1024*1024) + largeBlob := make([]byte, common.DefaultMaxBlobSize+1) res := cl.Submit(context.Background(), [][]byte{largeBlob}, 0, ns, nil) require.Equal(t, datypes.StatusTooBig, res.Code) diff --git a/block/internal/executing/executor.go b/block/internal/executing/executor.go index ed47b687bf..e71bf94811 100644 --- a/block/internal/executing/executor.go +++ b/block/internal/executing/executor.go @@ -49,10 +49,6 @@ type Executor struct { cache cache.Manager metrics *common.Metrics - // Broadcasting - headerBroadcaster common.HeaderP2PBroadcaster - dataBroadcaster common.DataP2PBroadcaster - // Configuration config config.Config genesis genesis.Genesis @@ -108,8 +104,6 @@ func NewExecutor( metrics *common.Metrics, config config.Config, genesis genesis.Genesis, - headerBroadcaster common.HeaderP2PBroadcaster, - dataBroadcaster common.DataP2PBroadcaster, logger zerolog.Logger, options common.BlockOptions, errorCh chan<- error, @@ -135,22 +129,20 @@ func NewExecutor( } e := &Executor{ - store: store, - exec: exec, - sequencer: sequencer, - signer: signer, - cache: cache, - metrics: metrics, - config: config, - genesis: genesis, - headerBroadcaster: headerBroadcaster, - dataBroadcaster: dataBroadcaster, - options: options, - lastState: &atomic.Pointer[types.State]{}, - raftNode: raftNode, - txNotifyCh: make(chan struct{}, 1), - errorCh: errorCh, - logger: logger.With().Str("component", "executor").Logger(), + store: store, + exec: exec, + sequencer: sequencer, + signer: signer, + cache: cache, + metrics: metrics, + config: config, + genesis: genesis, + options: options, + lastState: &atomic.Pointer[types.State]{}, + raftNode: raftNode, + txNotifyCh: make(chan struct{}, 1), + errorCh: errorCh, + logger: logger.With().Str("component", "executor").Logger(), } e.blockProducer = e return e, nil @@ -627,24 +619,6 @@ func (e *Executor) ProduceBlock(ctx context.Context) error { signature: signature, }) - // No broadcast to P2P when fiber is enabled. - if !e.config.DA.IsFiberEnabled() { - // Broadcast header and data to P2P network sequentially. - // IMPORTANT: Header MUST be broadcast before data — the P2P layer validates - // incoming data against the current and previous header, so out-of-order - // delivery would cause validation failures on peers. - if err := e.headerBroadcaster.WriteToStoreAndBroadcast(ctx, &types.P2PSignedHeader{ - SignedHeader: header, - }); err != nil { - e.logger.Error().Err(err).Msg("failed to broadcast header") - } - if err := e.dataBroadcaster.WriteToStoreAndBroadcast(ctx, &types.P2PData{ - Data: data, - }); err != nil { - e.logger.Error().Err(err).Msg("failed to broadcast data") - } - } - e.recordBlockMetrics(newState, data) e.logger.Info(). diff --git a/block/internal/executing/executor_benchmark_test.go b/block/internal/executing/executor_benchmark_test.go index be71d8fe26..219fa29b6d 100644 --- a/block/internal/executing/executor_benchmark_test.go +++ b/block/internal/executing/executor_benchmark_test.go @@ -8,10 +8,8 @@ import ( "testing" "time" - "github.com/celestiaorg/go-header" "github.com/ipfs/go-datastore" "github.com/ipfs/go-datastore/sync" - pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/crypto" "github.com/rs/zerolog" "github.com/stretchr/testify/require" @@ -24,7 +22,6 @@ import ( "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/pkg/signer/noop" "github.com/evstack/ev-node/pkg/store" - "github.com/evstack/ev-node/types" ) func BenchmarkProduceBlock(b *testing.B) { @@ -87,13 +84,11 @@ func newBenchExecutorWithStubs(b *testing.B, txs [][]byte) *Executor { stubExec := &stubExecClient{stateRoot: []byte("init_root")} stubSeq := &stubSequencer{txs: txs} - hb := &stubBroadcaster[*types.P2PSignedHeader]{} - db := &stubBroadcaster[*types.P2PData]{} exec, err := NewExecutor( memStore, stubExec, stubSeq, signerWrapper, cacheManager, common.NopMetrics(), cfg, gen, - hb, db, zerolog.Nop(), common.DefaultBlockOptions(), + zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), nil, ) require.NoError(b, err) @@ -159,12 +154,3 @@ func (s *stubExecClient) GetExecutionInfo(context.Context) (coreexec.ExecutionIn func (s *stubExecClient) FilterTxs(context.Context, [][]byte, uint64, uint64, bool) ([]coreexec.FilterStatus, error) { return nil, nil } - -// stubBroadcaster implements common.Broadcaster[H] with no-ops. -type stubBroadcaster[H header.Header[H]] struct{} - -func (s *stubBroadcaster[H]) WriteToStoreAndBroadcast(context.Context, H, ...pubsub.PubOpt) error { - return nil -} -func (s *stubBroadcaster[H]) Store() header.Store[H] { return nil } -func (s *stubBroadcaster[H]) Height() uint64 { return 0 } diff --git a/block/internal/executing/executor_lazy_test.go b/block/internal/executing/executor_lazy_test.go index 8c2cb8cef9..dd3cf0d82b 100644 --- a/block/internal/executing/executor_lazy_test.go +++ b/block/internal/executing/executor_lazy_test.go @@ -19,7 +19,6 @@ import ( "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/pkg/store" testmocks "github.com/evstack/ev-node/test/mocks" - "github.com/evstack/ev-node/types" ) func TestLazyMode_ProduceBlockLogic(t *testing.T) { @@ -47,10 +46,6 @@ func TestLazyMode_ProduceBlockLogic(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockSeq := testmocks.NewMockSequencer(t) - hb := common.NewMockBroadcaster[*types.P2PSignedHeader](t) - hb.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() - db := common.NewMockBroadcaster[*types.P2PData](t) - db.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() exec, err := NewExecutor( memStore, @@ -61,8 +56,6 @@ func TestLazyMode_ProduceBlockLogic(t *testing.T) { metrics, cfg, gen, - hb, - db, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), @@ -163,10 +156,6 @@ func TestRegularMode_ProduceBlockLogic(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockSeq := testmocks.NewMockSequencer(t) - hb := common.NewMockBroadcaster[*types.P2PSignedHeader](t) - hb.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() - db := common.NewMockBroadcaster[*types.P2PData](t) - db.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() exec, err := NewExecutor( memStore, @@ -177,8 +166,6 @@ func TestRegularMode_ProduceBlockLogic(t *testing.T) { metrics, cfg, gen, - hb, - db, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), diff --git a/block/internal/executing/executor_logic_test.go b/block/internal/executing/executor_logic_test.go index 1498bf5f79..fcb2ed53a0 100644 --- a/block/internal/executing/executor_logic_test.go +++ b/block/internal/executing/executor_logic_test.go @@ -270,13 +270,8 @@ func setupTestExecutor(t *testing.T, pendingLimit uint64) executorTestFixture { mockExec := testmocks.NewMockExecutor(t) mockSeq := testmocks.NewMockSequencer(t) - hb := common.NewMockBroadcaster[*types.P2PSignedHeader](t) - hb.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() - db := common.NewMockBroadcaster[*types.P2PData](t) - db.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() - exec, err := NewExecutor( - memStore, mockExec, mockSeq, signerWrapper, cacheManager, metrics, cfg, gen, hb, db, + memStore, mockExec, mockSeq, signerWrapper, cacheManager, metrics, cfg, gen, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), nil, ) require.NoError(t, err) diff --git a/block/internal/executing/executor_restart_test.go b/block/internal/executing/executor_restart_test.go index 51d940a630..770204e6ed 100644 --- a/block/internal/executing/executor_restart_test.go +++ b/block/internal/executing/executor_restart_test.go @@ -48,10 +48,6 @@ func TestExecutor_RestartUsesPendingHeader(t *testing.T) { // Create first executor instance mockExec1 := testmocks.NewMockExecutor(t) mockSeq1 := testmocks.NewMockSequencer(t) - hb1 := common.NewMockBroadcaster[*types.P2PSignedHeader](t) - hb1.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() - db1 := common.NewMockBroadcaster[*types.P2PData](t) - db1.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() exec1, err := NewExecutor( memStore, @@ -62,8 +58,6 @@ func TestExecutor_RestartUsesPendingHeader(t *testing.T) { metrics, cfg, gen, - hb1, - db1, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), @@ -167,10 +161,6 @@ func TestExecutor_RestartUsesPendingHeader(t *testing.T) { // Create second executor instance (restart scenario) mockExec2 := testmocks.NewMockExecutor(t) mockSeq2 := testmocks.NewMockSequencer(t) - hb2 := common.NewMockBroadcaster[*types.P2PSignedHeader](t) - hb2.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() - db2 := common.NewMockBroadcaster[*types.P2PData](t) - db2.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() exec2, err := NewExecutor( memStore, // same store @@ -181,8 +171,6 @@ func TestExecutor_RestartUsesPendingHeader(t *testing.T) { metrics, cfg, gen, - hb2, - db2, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), @@ -232,9 +220,6 @@ func TestExecutor_RestartUsesPendingHeader(t *testing.T) { // Verify that the header data hash matches the pending block assert.Equal(t, pendingData.DACommitment(), finalHeader.DataHash) - // Verify broadcasters were called with the pending block data - // The testify mock framework tracks calls automatically - // Verify the executor state was updated correctly finalState := exec2.getLastState() assert.Equal(t, uint64(2), finalState.LastBlockHeight) @@ -271,10 +256,6 @@ func TestExecutor_RestartNoPendingHeader(t *testing.T) { // Create first executor and produce one block mockExec1 := testmocks.NewMockExecutor(t) mockSeq1 := testmocks.NewMockSequencer(t) - hb1 := common.NewMockBroadcaster[*types.P2PSignedHeader](t) - hb1.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() - db1 := common.NewMockBroadcaster[*types.P2PData](t) - db1.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() exec1, err := NewExecutor( memStore, @@ -285,8 +266,6 @@ func TestExecutor_RestartNoPendingHeader(t *testing.T) { metrics, cfg, gen, - hb1, - db1, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), @@ -332,10 +311,6 @@ func TestExecutor_RestartNoPendingHeader(t *testing.T) { // Create second executor (restart) mockExec2 := testmocks.NewMockExecutor(t) mockSeq2 := testmocks.NewMockSequencer(t) - hb2 := common.NewMockBroadcaster[*types.P2PSignedHeader](t) - hb2.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() - db2 := common.NewMockBroadcaster[*types.P2PData](t) - db2.EXPECT().WriteToStoreAndBroadcast(mock.Anything, mock.Anything).Return(nil).Maybe() exec2, err := NewExecutor( memStore, @@ -346,8 +321,6 @@ func TestExecutor_RestartNoPendingHeader(t *testing.T) { metrics, cfg, gen, - hb2, - db2, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), diff --git a/block/internal/executing/executor_test.go b/block/internal/executing/executor_test.go index 1099cdb87d..4b04c8af10 100644 --- a/block/internal/executing/executor_test.go +++ b/block/internal/executing/executor_test.go @@ -15,22 +15,18 @@ import ( "github.com/evstack/ev-node/pkg/config" "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/pkg/store" - "github.com/evstack/ev-node/types" ) -func TestExecutor_BroadcasterIntegration(t *testing.T) { - // Create in-memory store +func TestExecutor_BasicProperties(t *testing.T) { ds := sync.MutexWrap(datastore.NewMapDatastore()) memStore := store.New(ds) - // Create cache cacheManager, err := cache.NewManager(config.DefaultConfig(), memStore, zerolog.Nop()) require.NoError(t, err) metrics := common.NopMetrics() signerAddr, _, testSigner := buildTestSigner(t) - // Create genesis gen := genesis.Genesis{ ChainID: "test-chain", InitialHeight: 1, @@ -38,73 +34,15 @@ func TestExecutor_BroadcasterIntegration(t *testing.T) { ProposerAddress: signerAddr, } - // Create mock broadcasters - headerBroadcaster := common.NewMockBroadcaster[*types.P2PSignedHeader](t) - dataBroadcaster := common.NewMockBroadcaster[*types.P2PData](t) - - // Create executor with broadcasters executor, err := NewExecutor( memStore, - nil, // nil executor (we're not testing execution) - nil, // nil sequencer (we're not testing sequencing) - testSigner, // test signer (required for executor) - cacheManager, - metrics, - config.DefaultConfig(), - gen, - headerBroadcaster, - dataBroadcaster, - zerolog.Nop(), - common.DefaultBlockOptions(), - make(chan error, 1), nil, - ) - require.NoError(t, err) - - // Verify broadcasters are set - assert.NotNil(t, executor.headerBroadcaster) - assert.NotNil(t, executor.dataBroadcaster) - assert.Equal(t, headerBroadcaster, executor.headerBroadcaster) - assert.Equal(t, dataBroadcaster, executor.dataBroadcaster) - - // Verify other properties - assert.Equal(t, memStore, executor.store) - assert.Equal(t, cacheManager, executor.cache) - assert.Equal(t, gen, executor.genesis) -} - -func TestExecutor_NilBroadcasters(t *testing.T) { - // Create in-memory store - ds := sync.MutexWrap(datastore.NewMapDatastore()) - memStore := store.New(ds) - - // Create cache - cacheManager, err := cache.NewManager(config.DefaultConfig(), memStore, zerolog.Nop()) - require.NoError(t, err) - - metrics := common.NopMetrics() - signerAddr, _, testSigner := buildTestSigner(t) - - // Create genesis - gen := genesis.Genesis{ - ChainID: "test-chain", - InitialHeight: 1, - StartTime: time.Now(), - ProposerAddress: signerAddr, - } - - // Create executor with nil broadcasters (light node scenario) - executor, err := NewExecutor( - memStore, - nil, // nil executor - nil, // nil sequencer - testSigner, // test signer (required for executor) + nil, + testSigner, cacheManager, metrics, config.DefaultConfig(), gen, - nil, // nil header broadcaster - nil, // nil data broadcaster zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), @@ -112,11 +50,6 @@ func TestExecutor_NilBroadcasters(t *testing.T) { ) require.NoError(t, err) - // Verify broadcasters are nil - assert.Nil(t, executor.headerBroadcaster) - assert.Nil(t, executor.dataBroadcaster) - - // Verify other properties assert.Equal(t, memStore, executor.store) assert.Equal(t, cacheManager, executor.cache) assert.Equal(t, gen, executor.genesis) diff --git a/block/internal/submitting/da_submitter.go b/block/internal/submitting/da_submitter.go index 2d6fa5ea18..eb92e38d5c 100644 --- a/block/internal/submitting/da_submitter.go +++ b/block/internal/submitting/da_submitter.go @@ -1,7 +1,6 @@ package submitting import ( - "bytes" "context" "encoding/json" "fmt" @@ -10,7 +9,6 @@ import ( "sync/atomic" "time" - lru "github.com/hashicorp/golang-lru/v2" "github.com/rs/zerolog" "github.com/evstack/ev-node/block/internal/cache" @@ -27,12 +25,11 @@ import ( const ( // DefaultEnvelopeCacheSize is the default size for caching signed DA envelopes. - // This avoids re-signing headers on retry scenarios. DefaultEnvelopeCacheSize = 10_000 // signingWorkerPoolSize determines how many parallel signing goroutines to use. - // Ed25519 signing is CPU-bound, so we use GOMAXPROCS workers. - signingWorkerPoolSize = 0 // 0 means use runtime.GOMAXPROCS(0) + // 0 means use runtime.GOMAXPROCS(0) + signingWorkerPoolSize = 0 ) const initialBackoff = 100 * time.Millisecond @@ -103,29 +100,19 @@ func clamp(v, minTime, maxTime time.Duration) time.Duration { return v } -type DAHintAppender interface { - AppendDAHint(ctx context.Context, daHeight uint64, heights ...uint64) error -} - // DASubmitter handles DA submission operations type DASubmitter struct { - client da.Client - config config.Config - genesis genesis.Genesis - options common.BlockOptions - logger zerolog.Logger - metrics *common.Metrics - headerDAHintAppender DAHintAppender - dataDAHintAppender DAHintAppender + client da.Client + config config.Config + genesis genesis.Genesis + options common.BlockOptions + logger zerolog.Logger + metrics *common.Metrics // address selector for multi-account support addressSelector pkgda.AddressSelector - // envelopeCache caches fully signed DA envelopes by height to avoid re-signing on retries - envelopeCache *lru.Cache[uint64, []byte] - - // lastSubmittedHeight tracks the last successfully submitted height for lazy cache invalidation. - // This avoids O(N) iteration over the cache on every submission. + // lastSubmittedHeight tracks the last successfully submitted height for lazy cache invalidation lastSubmittedHeight atomic.Uint64 // signingWorkers is the number of parallel workers for signing @@ -142,8 +129,6 @@ func NewDASubmitter( options common.BlockOptions, metrics *common.Metrics, logger zerolog.Logger, - headerDAHintAppender DAHintAppender, - dataDAHintAppender DAHintAppender, ) *DASubmitter { daSubmitterLogger := logger.With().Str("component", "da_submitter").Logger() @@ -168,12 +153,6 @@ func NewDASubmitter( addressSelector = pkgda.NewNoOpSelector() } - // Create envelope cache for avoiding re-signing on retries - envelopeCache, err := lru.New[uint64, []byte](DefaultEnvelopeCacheSize) - if err != nil { - daSubmitterLogger.Warn().Err(err).Msg("failed to create envelope cache, continuing without caching") - } - // Determine number of signing workers workers := signingWorkerPoolSize if workers <= 0 || workers > runtime.GOMAXPROCS(0) { @@ -181,17 +160,14 @@ func NewDASubmitter( } return &DASubmitter{ - client: client, - config: config, - genesis: genesis, - options: options, - metrics: metrics, - logger: daSubmitterLogger, - addressSelector: addressSelector, - envelopeCache: envelopeCache, - signingWorkers: workers, - headerDAHintAppender: headerDAHintAppender, - dataDAHintAppender: dataDAHintAppender, + client: client, + config: config, + genesis: genesis, + options: options, + metrics: metrics, + logger: daSubmitterLogger, + addressSelector: addressSelector, + signingWorkers: workers, } } @@ -199,8 +175,7 @@ func (s *DASubmitter) Close() { s.wg.Wait() } -// SubmitHeaders submits pending headers to DA layer -func (s *DASubmitter) SubmitHeaders(ctx context.Context, headers []*types.SignedHeader, marshalledHeaders [][]byte, cache cache.Manager, signer signer.Signer, onSubmitError func(error)) error { +func (s *DASubmitter) SubmitBlocks(ctx context.Context, headers []*types.SignedHeader, dataList []*types.Data, cacheMgr cache.Manager, signer signer.Signer, onSubmitError func(error)) error { if len(headers) == 0 { return nil } @@ -209,109 +184,64 @@ func (s *DASubmitter) SubmitHeaders(ctx context.Context, headers []*types.Signed return fmt.Errorf("signer is nil") } - if len(marshalledHeaders) != len(headers) { - return fmt.Errorf("marshalledHeaders length (%d) does not match headers length (%d)", len(marshalledHeaders), len(headers)) + if len(dataList) != len(headers) { + return fmt.Errorf("dataList length (%d) does not match headers length (%d)", len(dataList), len(headers)) } - s.logger.Info().Int("count", len(headers)).Msg("submitting headers to DA") + s.logger.Info().Int("count", len(headers)).Msg("submitting combined blocks to DA") - // Create DA envelopes with parallel signing and caching - envelopes, err := s.createDAEnvelopes(ctx, headers, marshalledHeaders, signer) - if err != nil { - return err - } + blobs := make([][]byte, 0, len(headers)) + submittedHeaders := make([]*types.SignedHeader, 0, len(headers)) + submittedData := make([]*types.Data, 0, len(headers)) - postSubmit := s.makeHeaderPostSubmit(ctx, cache) - namespace := s.client.GetHeaderNamespace() - submittedOffset := 0 - - s.wg.Go(func() { - s.submitWithRetry(ctx, envelopes, namespace, func(submittedCount int, daHeight uint64) { - if submittedCount > 0 { - end := submittedOffset + submittedCount - postSubmit(headers[submittedOffset:end], &datypes.ResultSubmit{BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess, SubmittedCount: uint64(submittedCount), Height: daHeight}}) - submittedOffset = end - } - }, onSubmitError, "header") - }) - - return nil -} + for i, header := range headers { + data := dataList[i] -func (s *DASubmitter) makeHeaderPostSubmit(ctx context.Context, cache cache.Manager) func([]*types.SignedHeader, *datypes.ResultSubmit) { - return func(submitted []*types.SignedHeader, res *datypes.ResultSubmit) { - heights := make([]uint64, len(submitted)) - for i, header := range submitted { - cache.SetHeaderDAIncluded(header.Hash().String(), res.Height, header.Height()) - heights[i] = header.Height() - } - if err := s.headerDAHintAppender.AppendDAHint(ctx, res.Height, heights...); err != nil { - s.logger.Error().Err(err).Msg("failed to append da height hint in header p2p store") - } - if l := len(submitted); l > 0 { - lastHeight := submitted[l-1].Height() - cache.SetLastSubmittedHeaderHeight(ctx, lastHeight) - s.lastSubmittedHeight.Store(lastHeight) + headerBz, err := header.MarshalBinary() + if err != nil { + return fmt.Errorf("failed to marshal header at height %d: %w", header.Height(), err) } - } -} - -// SubmitData submits pending data to DA layer -func (s *DASubmitter) SubmitData(ctx context.Context, unsignedDataList []*types.SignedData, marshalledData [][]byte, cache cache.Manager, signer signer.Signer, genesis genesis.Genesis, onSubmitError func(error)) error { - if len(unsignedDataList) == 0 { - return nil - } - if len(marshalledData) != len(unsignedDataList) { - return fmt.Errorf("marshalledData length (%d) does not match unsignedDataList length (%d)", len(marshalledData), len(unsignedDataList)) - } + envelopeSig, err := signer.Sign(ctx, headerBz) + if err != nil { + return fmt.Errorf("failed to sign envelope for header %d: %w", header.Height(), err) + } - // Sign the data (cache returns unsigned SignedData structs) - signedDataList, signedDataListBz, err := s.signData(ctx, unsignedDataList, marshalledData, signer, genesis) - if err != nil { - return fmt.Errorf("failed to sign data: %w", err) - } + blob, err := common.MarshalBlockBlob(header, data, envelopeSig) + if err != nil { + return fmt.Errorf("failed to create combined blob for height %d: %w", header.Height(), err) + } - if len(signedDataList) == 0 { - return nil + blobs = append(blobs, blob) + submittedHeaders = append(submittedHeaders, header) + submittedData = append(submittedData, data) } - s.logger.Info().Int("count", len(signedDataList)).Msg("submitting data to DA") - - postSubmit := s.makeDataPostSubmit(ctx, cache) - namespace := s.client.GetDataNamespace() + namespace := s.client.GetHeaderNamespace() submittedOffset := 0 s.wg.Go(func() { - s.submitWithRetry(ctx, signedDataListBz, namespace, func(submittedCount int, daHeight uint64) { + s.submitWithRetry(ctx, blobs, namespace, func(submittedCount int, daHeight uint64) { if submittedCount > 0 { end := submittedOffset + submittedCount - postSubmit(signedDataList[submittedOffset:end], &datypes.ResultSubmit{BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess, SubmittedCount: uint64(submittedCount), Height: daHeight}}) + for _, hdr := range submittedHeaders[submittedOffset:end] { + cacheMgr.SetHeaderDAIncluded(hdr.Hash().String(), daHeight, hdr.Height()) + cacheMgr.SetLastSubmittedHeaderHeight(ctx, hdr.Height()) + } + for _, d := range submittedData[submittedOffset:end] { + if len(d.Txs) > 0 { + cacheMgr.SetDataDAIncluded(d.DACommitment().String(), daHeight, d.Height()) + } + } + cacheMgr.SetLastSubmittedDataHeight(ctx, submittedHeaders[end-1].Height()) submittedOffset = end } - }, onSubmitError, "data") + }, onSubmitError, "block") }) return nil } -func (s *DASubmitter) makeDataPostSubmit(ctx context.Context, cache cache.Manager) func([]*types.SignedData, *datypes.ResultSubmit) { - return func(submitted []*types.SignedData, res *datypes.ResultSubmit) { - heights := make([]uint64, len(submitted)) - for i, sd := range submitted { - cache.SetDataDAIncluded(sd.Data.DACommitment().String(), res.Height, sd.Height()) - heights[i] = sd.Height() - } - if err := s.dataDAHintAppender.AppendDAHint(ctx, res.Height, heights...); err != nil { - s.logger.Error().Err(err).Msg("failed to append da height hint in data p2p store") - } - if l := len(submitted); l > 0 { - lastHeight := submitted[l-1].Height() - cache.SetLastSubmittedDataHeight(ctx, lastHeight) - } - } -} - func (s *DASubmitter) submitWithRetry( ctx context.Context, marshaled [][]byte, @@ -500,220 +430,6 @@ func (s *DASubmitter) recordFailure(reason common.DASubmitterFailureReason) { } } -// createDAEnvelopes creates signed DA envelopes for the given headers. -// It uses caching to avoid re-signing on retries and parallel signing for new envelopes. -func (s *DASubmitter) createDAEnvelopes(ctx context.Context, headers []*types.SignedHeader, marshalledHeaders [][]byte, signer signer.Signer) ([][]byte, error) { - envelopes := make([][]byte, len(headers)) - - // First pass: check cache for already-signed envelopes - var needSigning []int // indices that need signing - for i, header := range headers { - height := header.Height() - if cached := s.getCachedEnvelope(height); cached != nil { - envelopes[i] = cached - } else { - needSigning = append(needSigning, i) - } - } - - // If all envelopes were cached, we're done - if len(needSigning) == 0 { - s.logger.Debug().Int("cached", len(headers)).Msg("all envelopes retrieved from cache") - return envelopes, nil - } - - s.logger.Debug(). - Int("cached", len(headers)-len(needSigning)). - Int("to_sign", len(needSigning)). - Msg("signing DA envelopes") - - // For small batches, sign sequentially to avoid goroutine overhead - if len(needSigning) <= 2 || s.signingWorkers <= 1 { - // Send jobs - for _, i := range needSigning { - envelope, err := s.signAndCacheEnvelope(ctx, headers[i], marshalledHeaders[i], signer) - if err != nil { - return nil, fmt.Errorf("failed to create envelope for header %d: %w", i, err) - } - envelopes[i] = envelope - } - return envelopes, nil - } - - // Parallel signing for larger batches - return s.signEnvelopesParallel(ctx, headers, marshalledHeaders, envelopes, needSigning, signer) -} - -// signEnvelopesParallel signs envelopes in parallel using a worker pool. -func (s *DASubmitter) signEnvelopesParallel( - ctx context.Context, - headers []*types.SignedHeader, - marshalledHeaders [][]byte, - envelopes [][]byte, - needSigning []int, - signer signer.Signer, -) ([][]byte, error) { - type signJob struct { - index int - } - type signResult struct { - index int - envelope []byte - err error - } - - jobs := make(chan signJob, len(needSigning)) - results := make(chan signResult, len(needSigning)) - - // Start workers - numWorkers := min(s.signingWorkers, len(needSigning)) - var wg sync.WaitGroup - for range numWorkers { - wg.Go(func() { - for job := range jobs { - envelope, err := s.signAndCacheEnvelope(ctx, headers[job.index], marshalledHeaders[job.index], signer) - results <- signResult{index: job.index, envelope: envelope, err: err} - } - }) - } - - for _, i := range needSigning { - jobs <- signJob{index: i} - } - close(jobs) - - // Wait for workers to finish and close results - go func() { - wg.Wait() - close(results) - }() - - // Collect results - var firstErr error - for result := range results { - if result.err != nil && firstErr == nil { - firstErr = fmt.Errorf("failed to create envelope for header %d: %w", result.index, result.err) - continue - } - if result.err == nil { - envelopes[result.index] = result.envelope - } - } - - if firstErr != nil { - return nil, firstErr - } - - return envelopes, nil -} - -// signAndCacheEnvelope signs a single header and caches the result. -func (s *DASubmitter) signAndCacheEnvelope(ctx context.Context, header *types.SignedHeader, marshalledHeader []byte, signer signer.Signer) ([]byte, error) { - // Sign the pre-marshalled header content - envelopeSignature, err := signer.Sign(ctx, marshalledHeader) - if err != nil { - return nil, fmt.Errorf("failed to sign envelope: %w", err) - } - - // Create the envelope and marshal it - envelope, err := header.MarshalDAEnvelope(envelopeSignature) - if err != nil { - return nil, fmt.Errorf("failed to marshal DA envelope: %w", err) - } - - // Cache for potential retries - s.setCachedEnvelope(header.Height(), envelope) - - return envelope, nil -} - -// getCachedEnvelope retrieves a cached envelope for the given height. -// Uses lazy invalidation: entries at or below lastSubmittedHeight are considered invalid. -func (s *DASubmitter) getCachedEnvelope(height uint64) []byte { - if s.envelopeCache == nil { - return nil - } - // Lazy invalidation: don't return cached data for already-submitted heights - if height <= s.lastSubmittedHeight.Load() { - return nil - } - if envelope, ok := s.envelopeCache.Get(height); ok { - return envelope - } - return nil -} - -// setCachedEnvelope stores an envelope in the cache. -// Does not cache heights that have already been submitted. -func (s *DASubmitter) setCachedEnvelope(height uint64, envelope []byte) { - if s.envelopeCache == nil { - return - } - // Don't cache already-submitted heights - if height <= s.lastSubmittedHeight.Load() { - return - } - s.envelopeCache.Add(height, envelope) -} - -// signData signs unsigned SignedData structs returned from cache -func (s *DASubmitter) signData(ctx context.Context, unsignedDataList []*types.SignedData, unsignedDataListBz [][]byte, signer signer.Signer, genesis genesis.Genesis) ([]*types.SignedData, [][]byte, error) { - if signer == nil { - return nil, nil, fmt.Errorf("signer is nil") - } - - pubKey, err := signer.GetPublic() - if err != nil { - return nil, nil, fmt.Errorf("failed to get public key: %w", err) - } - - addr, err := signer.GetAddress() - if err != nil { - return nil, nil, fmt.Errorf("failed to get address: %w", err) - } - - if len(genesis.ProposerAddress) > 0 && !bytes.Equal(addr, genesis.ProposerAddress) { - return nil, nil, fmt.Errorf("signer address mismatch with genesis proposer") - } - - signerInfo := types.Signer{ - PubKey: pubKey, - Address: addr, - } - - signedDataList := make([]*types.SignedData, 0, len(unsignedDataList)) - signedDataListBz := make([][]byte, 0, len(unsignedDataListBz)) - - for i, unsignedData := range unsignedDataList { - // Skip empty data - if len(unsignedData.Txs) == 0 { - continue - } - - signature, err := signer.Sign(ctx, unsignedDataListBz[i]) - if err != nil { - return nil, nil, fmt.Errorf("failed to sign data: %w", err) - } - - signedData := &types.SignedData{ - Data: unsignedData.Data, - Signer: signerInfo, - Signature: signature, - } - - signedDataList = append(signedDataList, signedData) - - signedDataBz, err := signedData.MarshalBinary() - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal signed data: %w", err) - } - - signedDataListBz = append(signedDataListBz, signedDataBz) - } - - return signedDataList, signedDataListBz, nil -} - // mergeSubmitOptions merges the base submit options with a signing address. // If the base options are valid JSON, the signing address is added to the JSON object. // Otherwise, a new JSON object is created with just the signing address. @@ -740,7 +456,6 @@ func mergeSubmitOptions(baseOptions []byte, signingAddress string) ([]byte, erro } // Add or override the signing address - // Note: Uses "signer_address" to match Celestia's TxConfig JSON schema optionsMap["signer_address"] = signingAddress // Marshal back to JSON diff --git a/block/internal/submitting/da_submitter_integration_test.go b/block/internal/submitting/da_submitter_integration_test.go index 1de6cc2559..8c57afd0b2 100644 --- a/block/internal/submitting/da_submitter_integration_test.go +++ b/block/internal/submitting/da_submitter_integration_test.go @@ -1,128 +1,34 @@ package submitting import ( - "context" - crand "crypto/rand" "testing" - "time" - "github.com/ipfs/go-datastore" - "github.com/ipfs/go-datastore/sync" - "github.com/libp2p/go-libp2p/core/crypto" "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/evstack/ev-node/block/internal/cache" "github.com/evstack/ev-node/block/internal/common" "github.com/evstack/ev-node/pkg/config" - datypes "github.com/evstack/ev-node/pkg/da/types" "github.com/evstack/ev-node/pkg/genesis" - "github.com/evstack/ev-node/pkg/signer/noop" - "github.com/evstack/ev-node/pkg/store" + "github.com/evstack/ev-node/pkg/rpc/server" "github.com/evstack/ev-node/test/mocks" - "github.com/evstack/ev-node/types" ) -func TestDASubmitter_SubmitHeadersAndData_MarksInclusionAndUpdatesLastSubmitted(t *testing.T) { - ds := sync.MutexWrap(datastore.NewMapDatastore()) - st := store.New(ds) - cm, err := cache.NewManager(config.DefaultConfig(), st, zerolog.Nop()) - require.NoError(t, err) - - // signer and proposer - priv, _, err := crypto.GenerateEd25519Key(crand.Reader) - require.NoError(t, err) - n, err := noop.NewNoopSigner(priv) - require.NoError(t, err) - addr, err := n.GetAddress() - require.NoError(t, err) - pub, err := n.GetPublic() - require.NoError(t, err) +func TestNewDASubmitterSetsVisualizerWhenEnabled(t *testing.T) { + t.Helper() cfg := config.DefaultConfig() - cfg.DA.Namespace = "ns-header" - cfg.DA.DataNamespace = "ns-data" - - gen := genesis.Genesis{ChainID: "chain1", InitialHeight: 1, StartTime: time.Now(), ProposerAddress: addr} - - // seed store with two heights - stateRoot := []byte{1, 2, 3} - // height 1 - hdr1 := &types.SignedHeader{Header: types.Header{BaseHeader: types.BaseHeader{ChainID: gen.ChainID, Height: 1, Time: uint64(time.Now().UnixNano())}, AppHash: stateRoot, ProposerAddress: addr}, Signer: types.Signer{PubKey: pub, Address: addr}} - bz1, err := types.DefaultAggregatorNodeSignatureBytesProvider(&hdr1.Header) - require.NoError(t, err) - sig1, err := n.Sign(t.Context(), bz1) - require.NoError(t, err) - hdr1.Signature = sig1 - data1 := &types.Data{Metadata: &types.Metadata{ChainID: gen.ChainID, Height: 1, Time: uint64(time.Now().UnixNano())}, Txs: types.Txs{types.Tx("a")}} - // height 2 - hdr2 := &types.SignedHeader{Header: types.Header{BaseHeader: types.BaseHeader{ChainID: gen.ChainID, Height: 2, Time: uint64(time.Now().Add(time.Second).UnixNano())}, AppHash: stateRoot, ProposerAddress: addr}, Signer: types.Signer{PubKey: pub, Address: addr}} - bz2, err := types.DefaultAggregatorNodeSignatureBytesProvider(&hdr2.Header) - require.NoError(t, err) - sig2, err := n.Sign(t.Context(), bz2) - require.NoError(t, err) - hdr2.Signature = sig2 - data2 := &types.Data{Metadata: &types.Metadata{ChainID: gen.ChainID, Height: 2, Time: uint64(time.Now().Add(time.Second).UnixNano())}, Txs: types.Txs{types.Tx("b")}} - - // persist to store - sig1t := types.Signature(sig1) - sig2t := types.Signature(sig2) - - // Save block 1 - batch1, err := st.NewBatch(context.Background()) - require.NoError(t, err) - require.NoError(t, batch1.SaveBlockData(hdr1, data1, &sig1t)) - require.NoError(t, batch1.SetHeight(1)) - require.NoError(t, batch1.Commit()) - - // Save block 2 - batch2, err := st.NewBatch(context.Background()) - require.NoError(t, err) - require.NoError(t, batch2.SaveBlockData(hdr2, data2, &sig2t)) - require.NoError(t, batch2.SetHeight(2)) - require.NoError(t, batch2.Commit()) - - // Mock DA client - client := mocks.NewMockClient(t) - headerNs := datypes.NamespaceFromString(cfg.DA.Namespace).Bytes() - dataNs := datypes.NamespaceFromString(cfg.DA.DataNamespace).Bytes() - client.On("GetHeaderNamespace").Return(headerNs).Maybe() - client.On("GetDataNamespace").Return(dataNs).Maybe() - client.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() - client.On("HasForcedInclusionNamespace").Return(false).Maybe() - client.On("Submit", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(func(_ context.Context, blobs [][]byte, _ float64, _ []byte, _ []byte) datypes.ResultSubmit { - return datypes.ResultSubmit{BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess, SubmittedCount: uint64(len(blobs)), Height: 1}} - }).Twice() - daSubmitter := NewDASubmitter(client, cfg, gen, common.DefaultBlockOptions(), common.NopMetrics(), zerolog.Nop(), noopDAHintAppender{}, noopDAHintAppender{}) - - // Submit headers and data - cache returns both items and marshalled bytes - headers, marshalledHeaders, err := cm.GetPendingHeaders(context.Background()) - require.NoError(t, err) - require.NoError(t, daSubmitter.SubmitHeaders(context.Background(), headers, marshalledHeaders, cm, n, nil)) - - dataList, marshalledData, err := cm.GetPendingData(context.Background()) - require.NoError(t, err) - require.NoError(t, daSubmitter.SubmitData(context.Background(), dataList, marshalledData, cm, n, gen, nil)) - - daSubmitter.Close() - - // After submission, inclusion markers should be set - _, ok := cm.GetHeaderDAIncludedByHeight(1) - assert.True(t, ok) - _, ok = cm.GetHeaderDAIncludedByHeight(2) - assert.True(t, ok) - _, ok = cm.GetDataDAIncludedByHeight(1) - assert.True(t, ok) - _, ok = cm.GetDataDAIncludedByHeight(2) - assert.True(t, ok) - -} - -type noopDAHintAppender struct{} - -func (n noopDAHintAppender) AppendDAHint(ctx context.Context, daHeight uint64, heights ...uint64) error { - return nil + cfg.RPC.EnableDAVisualization = true + cfg.Node.Aggregator = true + + daClient := mocks.NewMockClient(t) + NewDASubmitter( + daClient, + cfg, + genesis.Genesis{}, + common.DefaultBlockOptions(), + common.NopMetrics(), + zerolog.Nop(), + ) + + require.NotNil(t, server.GetDAVisualizationServer()) } diff --git a/block/internal/submitting/da_submitter_mocks_test.go b/block/internal/submitting/da_submitter_mocks_test.go index 021e551210..48f59c4400 100644 --- a/block/internal/submitting/da_submitter_mocks_test.go +++ b/block/internal/submitting/da_submitter_mocks_test.go @@ -33,7 +33,7 @@ func newTestBatchSubmitter(t *testing.T, mockClient *mocks.MockClient, override mockClient.On("GetDataNamespace").Return([]byte(cfg.DA.DataNamespace)).Maybe() mockClient.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() mockClient.On("HasForcedInclusionNamespace").Return(false).Maybe() - return NewDASubmitter(mockClient, cfg, genesis.Genesis{}, common.BlockOptions{}, common.NopMetrics(), zerolog.Nop(), nil, nil) + return NewDASubmitter(mockClient, cfg, genesis.Genesis{}, common.BlockOptions{}, common.NopMetrics(), zerolog.Nop()) } func TestSubmitWithRetry_MempoolRetry_Succeeds(t *testing.T) { diff --git a/block/internal/submitting/da_submitter_test.go b/block/internal/submitting/da_submitter_test.go index 7397f30445..412866bc49 100644 --- a/block/internal/submitting/da_submitter_test.go +++ b/block/internal/submitting/da_submitter_test.go @@ -1,30 +1,21 @@ package submitting import ( - "context" - crand "crypto/rand" "testing" "time" "github.com/ipfs/go-datastore" "github.com/ipfs/go-datastore/sync" - "github.com/libp2p/go-libp2p/core/crypto" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/evstack/ev-node/block/internal/cache" "github.com/evstack/ev-node/block/internal/common" "github.com/evstack/ev-node/pkg/config" - datypes "github.com/evstack/ev-node/pkg/da/types" "github.com/evstack/ev-node/pkg/genesis" - "github.com/evstack/ev-node/pkg/rpc/server" - "github.com/evstack/ev-node/pkg/signer" - "github.com/evstack/ev-node/pkg/signer/noop" "github.com/evstack/ev-node/pkg/store" "github.com/evstack/ev-node/test/mocks" - "github.com/evstack/ev-node/types" ) const ( @@ -45,12 +36,6 @@ func setupDASubmitterTest(t *testing.T) (*DASubmitter, store.Store, cache.Manage cfg.DA.DataNamespace = testDataNamespace mockDA := mocks.NewMockClient(t) - headerNamespace := datypes.NamespaceFromString(cfg.DA.Namespace).Bytes() - dataNamespace := datypes.NamespaceFromString(cfg.DA.DataNamespace).Bytes() - mockDA.On("GetHeaderNamespace").Return(headerNamespace).Maybe() - mockDA.On("GetDataNamespace").Return(dataNamespace).Maybe() - mockDA.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() - mockDA.On("HasForcedInclusionNamespace").Return(false).Maybe() gen := genesis.Genesis{ ChainID: "test-chain", @@ -66,26 +51,11 @@ func setupDASubmitterTest(t *testing.T) (*DASubmitter, store.Store, cache.Manage common.DefaultBlockOptions(), common.NopMetrics(), zerolog.Nop(), - noopDAHintAppender{}, - noopDAHintAppender{}, ) return daSubmitter, st, cm, mockDA, gen } -func createTestSigner(t *testing.T) ([]byte, crypto.PubKey, signer.Signer) { - t.Helper() - priv, _, err := crypto.GenerateEd25519Key(crand.Reader) - require.NoError(t, err) - signer, err := noop.NewNoopSigner(priv) - require.NoError(t, err) - addr, err := signer.GetAddress() - require.NoError(t, err) - pub, err := signer.GetPublic() - require.NoError(t, err) - return addr, pub, signer -} - func TestDASubmitter_NewDASubmitter(t *testing.T) { submitter, _, _, _, _ := setupDASubmitterTest(t) @@ -94,451 +64,3 @@ func TestDASubmitter_NewDASubmitter(t *testing.T) { assert.NotNil(t, submitter.config) assert.NotNil(t, submitter.genesis) } - -func TestNewDASubmitterSetsVisualizerWhenEnabled(t *testing.T) { - t.Helper() - defer server.SetDAVisualizationServer(nil) - - cfg := config.DefaultConfig() - cfg.RPC.EnableDAVisualization = true - cfg.Node.Aggregator = true - - daClient := mocks.NewMockClient(t) - daClient.On("GetHeaderNamespace").Return(datypes.NamespaceFromString(cfg.DA.Namespace).Bytes()).Maybe() - daClient.On("GetDataNamespace").Return(datypes.NamespaceFromString(cfg.DA.DataNamespace).Bytes()).Maybe() - daClient.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() - daClient.On("HasForcedInclusionNamespace").Return(false).Maybe() - NewDASubmitter( - daClient, - cfg, - genesis.Genesis{}, - common.DefaultBlockOptions(), - common.NopMetrics(), - zerolog.Nop(), - noopDAHintAppender{}, - noopDAHintAppender{}, - ) - - require.NotNil(t, server.GetDAVisualizationServer()) -} - -func TestDASubmitter_SubmitHeaders_Success(t *testing.T) { - submitter, st, cm, mockDA, gen := setupDASubmitterTest(t) - ctx := context.Background() - headerNamespace := datypes.NamespaceFromString(testHeaderNamespace).Bytes() - - mockDA.On( - "Submit", - mock.Anything, - mock.AnythingOfType("[][]uint8"), - mock.AnythingOfType("float64"), - headerNamespace, - mock.Anything, - ).Return(func(_ context.Context, blobs [][]byte, _ float64, _ []byte, _ []byte) datypes.ResultSubmit { - return datypes.ResultSubmit{BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess, SubmittedCount: uint64(len(blobs)), Height: 1}} - }).Once() - - // Create test signer - addr, pub, signer := createTestSigner(t) - - // Create test headers - header1 := &types.SignedHeader{ - Header: types.Header{ - BaseHeader: types.BaseHeader{ - ChainID: gen.ChainID, - Height: 1, - Time: uint64(time.Now().UnixNano()), - }, - ProposerAddress: addr, - }, - Signer: types.Signer{PubKey: pub, Address: addr}, - } - - header2 := &types.SignedHeader{ - Header: types.Header{ - BaseHeader: types.BaseHeader{ - ChainID: gen.ChainID, - Height: 2, - Time: uint64(time.Now().UnixNano()), - }, - ProposerAddress: addr, - }, - Signer: types.Signer{PubKey: pub, Address: addr}, - } - - // Sign headers - for _, header := range []*types.SignedHeader{header1, header2} { - bz, err := types.DefaultAggregatorNodeSignatureBytesProvider(&header.Header) - require.NoError(t, err) - sig, err := signer.Sign(t.Context(), bz) - require.NoError(t, err) - header.Signature = sig - } - - // Create empty data for the headers - data1 := &types.Data{ - Metadata: &types.Metadata{ - ChainID: gen.ChainID, - Height: 1, - Time: uint64(time.Now().UnixNano()), - }, - } - - data2 := &types.Data{ - Metadata: &types.Metadata{ - ChainID: gen.ChainID, - Height: 2, - Time: uint64(time.Now().UnixNano()), - }, - } - - // Save to store to make them pending - sig1 := header1.Signature - sig2 := header2.Signature - - // Save block 1 - batch1, err := st.NewBatch(ctx) - require.NoError(t, err) - require.NoError(t, batch1.SaveBlockData(header1, data1, &sig1)) - require.NoError(t, batch1.SetHeight(1)) - require.NoError(t, batch1.Commit()) - - // Save block 2 - batch2, err := st.NewBatch(ctx) - require.NoError(t, err) - require.NoError(t, batch2.SaveBlockData(header2, data2, &sig2)) - require.NoError(t, batch2.SetHeight(2)) - require.NoError(t, batch2.Commit()) - - // Get headers from cache and submit - headers, marshalledHeaders, err := cm.GetPendingHeaders(ctx) - require.NoError(t, err) - err = submitter.SubmitHeaders(ctx, headers, marshalledHeaders, cm, signer, nil) - require.NoError(t, err) - submitter.Close() - - _, ok1 := cm.GetHeaderDAIncludedByHeight(1) - assert.True(t, ok1) - _, ok2 := cm.GetHeaderDAIncludedByHeight(2) - assert.True(t, ok2) -} - -func TestDASubmitter_SubmitHeaders_NoPendingHeaders(t *testing.T) { - submitter, _, cm, mockDA, _ := setupDASubmitterTest(t) - ctx := context.Background() - - // Create test signer - _, _, signer := createTestSigner(t) - - // Get headers from cache (should be empty) and submit - headers, marshalledHeaders, err := cm.GetPendingHeaders(ctx) - require.NoError(t, err) - err = submitter.SubmitHeaders(ctx, headers, marshalledHeaders, cm, signer, nil) - require.NoError(t, err) // Should succeed with no action - mockDA.AssertNotCalled(t, "Submit", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) -} - -func TestDASubmitter_SubmitData_Success(t *testing.T) { - submitter, st, cm, mockDA, gen := setupDASubmitterTest(t) - ctx := context.Background() - dataNamespace := datypes.NamespaceFromString(testDataNamespace).Bytes() - - mockDA.On( - "Submit", - mock.Anything, - mock.AnythingOfType("[][]uint8"), - mock.AnythingOfType("float64"), - dataNamespace, - mock.Anything, - ).Return(func(_ context.Context, blobs [][]byte, _ float64, _ []byte, _ []byte) datypes.ResultSubmit { - return datypes.ResultSubmit{BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess, SubmittedCount: uint64(len(blobs)), Height: 2}} - }).Once() - - // Create test signer - addr, pub, signer := createTestSigner(t) - gen.ProposerAddress = addr - - // Update submitter genesis to use correct proposer - submitter.genesis.ProposerAddress = addr - - // Create test data with transactions - data1 := &types.Data{ - Metadata: &types.Metadata{ - ChainID: gen.ChainID, - Height: 1, - Time: uint64(time.Now().UnixNano()), - }, - Txs: types.Txs{types.Tx("tx1"), types.Tx("tx2")}, - } - - data2 := &types.Data{ - Metadata: &types.Metadata{ - ChainID: gen.ChainID, - Height: 2, - Time: uint64(time.Now().UnixNano()), - }, - Txs: types.Txs{types.Tx("tx3")}, - } - - // Create headers for the data - header1 := &types.SignedHeader{ - Header: types.Header{ - BaseHeader: types.BaseHeader{ - ChainID: gen.ChainID, - Height: 1, - Time: uint64(time.Now().UnixNano()), - }, - ProposerAddress: addr, - DataHash: data1.DACommitment(), - }, - Signer: types.Signer{PubKey: pub, Address: addr}, - } - - header2 := &types.SignedHeader{ - Header: types.Header{ - BaseHeader: types.BaseHeader{ - ChainID: gen.ChainID, - Height: 2, - Time: uint64(time.Now().UnixNano()), - }, - ProposerAddress: addr, - DataHash: data2.DACommitment(), - }, - Signer: types.Signer{PubKey: pub, Address: addr}, - } - - // Save to store to make them pending - sig1 := types.Signature([]byte("sig1")) - sig2 := types.Signature([]byte("sig2")) - - // Save block 1 - batch1, err := st.NewBatch(ctx) - require.NoError(t, err) - require.NoError(t, batch1.SaveBlockData(header1, data1, &sig1)) - require.NoError(t, batch1.SetHeight(1)) - require.NoError(t, batch1.Commit()) - - // Save block 2 - batch2, err := st.NewBatch(ctx) - require.NoError(t, err) - require.NoError(t, batch2.SaveBlockData(header2, data2, &sig2)) - require.NoError(t, batch2.SetHeight(2)) - require.NoError(t, batch2.Commit()) - - // Get data from cache and submit - signedDataList, marshalledData, err := cm.GetPendingData(ctx) - require.NoError(t, err) - err = submitter.SubmitData(ctx, signedDataList, marshalledData, cm, signer, gen, nil) - require.NoError(t, err) - submitter.Close() - - // Verify data is marked as DA included - _, ok := cm.GetDataDAIncludedByHeight(1) - assert.True(t, ok) - _, ok = cm.GetDataDAIncludedByHeight(2) - assert.True(t, ok) -} - -func TestDASubmitter_SubmitData_SkipsEmptyData(t *testing.T) { - submitter, st, cm, mockDA, gen := setupDASubmitterTest(t) - ctx := context.Background() - - // Create test signer - addr, pub, signer := createTestSigner(t) - gen.ProposerAddress = addr - - // Create empty data - emptyData := &types.Data{ - Metadata: &types.Metadata{ - ChainID: gen.ChainID, - Height: 1, - Time: uint64(time.Now().UnixNano()), - }, - Txs: types.Txs{}, // Empty - } - - // Create header for the empty data - header := &types.SignedHeader{ - Header: types.Header{ - BaseHeader: types.BaseHeader{ - ChainID: gen.ChainID, - Height: 1, - Time: uint64(time.Now().UnixNano()), - }, - ProposerAddress: addr, - DataHash: common.DataHashForEmptyTxs, - }, - Signer: types.Signer{PubKey: pub, Address: addr}, - } - - // Save to store - sig := types.Signature([]byte("sig")) - batch, err := st.NewBatch(ctx) - require.NoError(t, err) - require.NoError(t, batch.SaveBlockData(header, emptyData, &sig)) - require.NoError(t, batch.SetHeight(1)) - require.NoError(t, batch.Commit()) - - // Get data from cache and submit - should succeed but skip empty data - // Get data from cache and submit - signedDataList, marshalledData, err := cm.GetPendingData(ctx) - require.NoError(t, err) - err = submitter.SubmitData(ctx, signedDataList, marshalledData, cm, signer, gen, nil) - require.NoError(t, err) - mockDA.AssertNotCalled(t, "Submit", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) - - // Empty data should not be marked as DA included (it's implicitly included) - _, ok := cm.GetDataDAIncludedByHash(emptyData.DACommitment().String()) - assert.False(t, ok) -} - -func TestDASubmitter_SubmitData_NoPendingData(t *testing.T) { - submitter, _, cm, mockDA, gen := setupDASubmitterTest(t) - ctx := context.Background() - - // Create test signer - _, _, signer := createTestSigner(t) - - // Get data from cache (should be empty) and submit - dataList, marshalledData, err := cm.GetPendingData(ctx) - require.NoError(t, err) - err = submitter.SubmitData(ctx, dataList, marshalledData, cm, signer, gen, nil) - require.NoError(t, err) // Should succeed with no action - mockDA.AssertNotCalled(t, "Submit", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) -} - -func TestDASubmitter_SubmitData_NilSigner(t *testing.T) { - submitter, st, cm, mockDA, gen := setupDASubmitterTest(t) - ctx := context.Background() - - // Create test data with transactions - data := &types.Data{ - Metadata: &types.Metadata{ - ChainID: gen.ChainID, - Height: 1, - Time: uint64(time.Now().UnixNano()), - }, - Txs: types.Txs{types.Tx("tx1")}, - } - - header := &types.SignedHeader{ - Header: types.Header{ - BaseHeader: types.BaseHeader{ - ChainID: gen.ChainID, - Height: 1, - Time: uint64(time.Now().UnixNano()), - }, - DataHash: data.DACommitment(), - }, - } - - // Save to store - sig := types.Signature([]byte("sig")) - batch, err := st.NewBatch(ctx) - require.NoError(t, err) - require.NoError(t, batch.SaveBlockData(header, data, &sig)) - require.NoError(t, batch.SetHeight(1)) - require.NoError(t, batch.Commit()) - - // Get data from cache and submit with nil signer - should fail - signedDataList, marshalledData, err := cm.GetPendingData(ctx) - require.NoError(t, err) - err = submitter.SubmitData(ctx, signedDataList, marshalledData, cm, nil, gen, nil) - require.Error(t, err) - assert.Contains(t, err.Error(), "signer is nil") - mockDA.AssertNotCalled(t, "Submit", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) -} - -func TestDASubmitter_SignData(t *testing.T) { - submitter, _, _, _, gen := setupDASubmitterTest(t) - - // Create test signer - addr, _, signer := createTestSigner(t) - gen.ProposerAddress = addr - - // Create test data - signedData1 := &types.SignedData{ - Data: types.Data{ - Metadata: &types.Metadata{ - ChainID: gen.ChainID, - Height: 1, - Time: uint64(time.Now().UnixNano()), - }, - Txs: types.Txs{types.Tx("tx1")}, - }, - } - - signedData2 := &types.SignedData{ - Data: types.Data{ - Metadata: &types.Metadata{ - ChainID: gen.ChainID, - Height: 2, - Time: uint64(time.Now().UnixNano()), - }, - Txs: types.Txs{}, // Empty - should be skipped - }, - } - - signedData3 := &types.SignedData{ - Data: types.Data{ - Metadata: &types.Metadata{ - ChainID: gen.ChainID, - Height: 3, - Time: uint64(time.Now().UnixNano()), - }, - Txs: types.Txs{types.Tx("tx3")}, - }, - } - - dataList := []*types.SignedData{signedData1, signedData2, signedData3} - dataListBz := make([][]byte, 0, len(dataList)) - for _, d := range dataList { - dataBz, err := d.MarshalBinary() - require.NoError(t, err) - dataListBz = append(dataListBz, dataBz) - } - - // Create signed data - resultData, resultDataBz, err := submitter.signData(t.Context(), dataList, dataListBz, signer, gen) - require.NoError(t, err) - - // Should have 2 items (empty data skipped) - assert.Len(t, resultData, 2) - assert.Len(t, resultDataBz, 2) - - // Verify signatures are set - for _, signedData := range resultData { - assert.NotEmpty(t, signedData.Signature) - assert.NotNil(t, signedData.Signer.PubKey) - assert.Equal(t, gen.ProposerAddress, signedData.Signer.Address) - } -} - -func TestDASubmitter_SignData_NilSigner(t *testing.T) { - submitter, _, _, _, gen := setupDASubmitterTest(t) - - // Create test data - signedData := &types.SignedData{ - Data: types.Data{ - Metadata: &types.Metadata{ - ChainID: gen.ChainID, - Height: 1, - }, - Txs: types.Txs{types.Tx("tx1")}, - }, - } - - dataList := []*types.SignedData{signedData} - - dataListBz := make([][]byte, 0, len(dataList)) - for _, d := range dataList { - dataBz, err := d.MarshalBinary() - require.NoError(t, err) - dataListBz = append(dataListBz, dataBz) - } - - // Create signed data with nil signer - should fail - _, _, err := submitter.signData(t.Context(), dataList, dataListBz, nil, gen) - require.Error(t, err) - assert.Contains(t, err.Error(), "signer is nil") -} diff --git a/block/internal/submitting/da_submitter_tracing.go b/block/internal/submitting/da_submitter_tracing.go index 155dd92dff..b6ee4b6af9 100644 --- a/block/internal/submitting/da_submitter_tracing.go +++ b/block/internal/submitting/da_submitter_tracing.go @@ -9,7 +9,6 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/evstack/ev-node/block/internal/cache" - "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/pkg/signer" "github.com/evstack/ev-node/types" ) @@ -30,70 +29,17 @@ func WithTracingDASubmitter(inner DASubmitterAPI) DASubmitterAPI { } } -func (t *tracedDASubmitter) SubmitHeaders(ctx context.Context, headers []*types.SignedHeader, marshalledHeaders [][]byte, cache cache.Manager, signer signer.Signer, onSubmitError func(error)) error { - ctx, span := t.tracer.Start(ctx, "DASubmitter.SubmitHeaders", +func (t *tracedDASubmitter) SubmitBlocks(ctx context.Context, headers []*types.SignedHeader, data []*types.Data, cacheMgr cache.Manager, signer signer.Signer, onSubmitError func(error)) error { + ctx, span := t.tracer.Start(ctx, "DASubmitter.SubmitBlocks", trace.WithAttributes( - attribute.Int("header.count", len(headers)), + attribute.Int("block.count", len(headers)), ), ) - var totalBytes int - for _, h := range marshalledHeaders { - totalBytes += len(h) - } - span.SetAttributes(attribute.Int("header.total_bytes", totalBytes)) - if len(headers) > 0 { span.SetAttributes( - attribute.Int64("header.start_height", int64(headers[0].Height())), - attribute.Int64("header.end_height", int64(headers[len(headers)-1].Height())), - ) - } - - var wrappedOnError func(error) - if onSubmitError != nil { - wrappedOnError = func(err error) { - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - } - span.End() - onSubmitError(err) - } - } - - err := t.inner.SubmitHeaders(ctx, headers, marshalledHeaders, cache, signer, wrappedOnError) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - span.End() - return err - } - - if onSubmitError == nil { - span.End() - } - - return nil -} - -func (t *tracedDASubmitter) SubmitData(ctx context.Context, signedDataList []*types.SignedData, marshalledData [][]byte, cache cache.Manager, signer signer.Signer, genesis genesis.Genesis, onSubmitError func(error)) error { - ctx, span := t.tracer.Start(ctx, "DASubmitter.SubmitData", - trace.WithAttributes( - attribute.Int("data.count", len(signedDataList)), - ), - ) - - var totalBytes int - for _, d := range marshalledData { - totalBytes += len(d) - } - span.SetAttributes(attribute.Int("data.total_bytes", totalBytes)) - - if len(signedDataList) > 0 { - span.SetAttributes( - attribute.Int64("data.start_height", int64(signedDataList[0].Height())), - attribute.Int64("data.end_height", int64(signedDataList[len(signedDataList)-1].Height())), + attribute.Int64("block.start_height", int64(headers[0].Height())), + attribute.Int64("block.end_height", int64(headers[len(headers)-1].Height())), ) } @@ -109,7 +55,7 @@ func (t *tracedDASubmitter) SubmitData(ctx context.Context, signedDataList []*ty } } - err := t.inner.SubmitData(ctx, signedDataList, marshalledData, cache, signer, genesis, wrappedOnError) + err := t.inner.SubmitBlocks(ctx, headers, data, cacheMgr, signer, wrappedOnError) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) diff --git a/block/internal/submitting/da_submitter_tracing_test.go b/block/internal/submitting/da_submitter_tracing_test.go index 36326dc575..2380a150fb 100644 --- a/block/internal/submitting/da_submitter_tracing_test.go +++ b/block/internal/submitting/da_submitter_tracing_test.go @@ -12,27 +12,18 @@ import ( "go.opentelemetry.io/otel/sdk/trace/tracetest" "github.com/evstack/ev-node/block/internal/cache" - "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/pkg/signer" "github.com/evstack/ev-node/pkg/telemetry/testutil" "github.com/evstack/ev-node/types" ) type mockDASubmitterAPI struct { - submitHeadersFn func(ctx context.Context, headers []*types.SignedHeader, marshalledHeaders [][]byte, cache cache.Manager, signer signer.Signer, onSubmitError func(error)) error - submitDataFn func(ctx context.Context, signedDataList []*types.SignedData, marshalledData [][]byte, cache cache.Manager, signer signer.Signer, genesis genesis.Genesis, onSubmitError func(error)) error + submitBlocksFn func(ctx context.Context, headers []*types.SignedHeader, data []*types.Data, cacheMgr cache.Manager, signer signer.Signer, onSubmitError func(error)) error } -func (m *mockDASubmitterAPI) SubmitHeaders(ctx context.Context, headers []*types.SignedHeader, marshalledHeaders [][]byte, cache cache.Manager, signer signer.Signer, onSubmitError func(error)) error { - if m.submitHeadersFn != nil { - return m.submitHeadersFn(ctx, headers, marshalledHeaders, cache, signer, onSubmitError) - } - return nil -} - -func (m *mockDASubmitterAPI) SubmitData(ctx context.Context, signedDataList []*types.SignedData, marshalledData [][]byte, cache cache.Manager, signer signer.Signer, genesis genesis.Genesis, onSubmitError func(error)) error { - if m.submitDataFn != nil { - return m.submitDataFn(ctx, signedDataList, marshalledData, cache, signer, genesis, onSubmitError) +func (m *mockDASubmitterAPI) SubmitBlocks(ctx context.Context, headers []*types.SignedHeader, data []*types.Data, cacheMgr cache.Manager, signer signer.Signer, onSubmitError func(error)) error { + if m.submitBlocksFn != nil { + return m.submitBlocksFn(ctx, headers, data, cacheMgr, signer, onSubmitError) } return nil } @@ -48,9 +39,9 @@ func setupDASubmitterTrace(t *testing.T, inner DASubmitterAPI) (DASubmitterAPI, return WithTracingDASubmitter(inner), sr } -func TestTracedDASubmitter_SubmitHeaders_Success(t *testing.T) { +func TestTracedDASubmitter_SubmitBlocks_Success(t *testing.T) { mock := &mockDASubmitterAPI{ - submitHeadersFn: func(ctx context.Context, headers []*types.SignedHeader, marshalledHeaders [][]byte, cache cache.Manager, signer signer.Signer, onSubmitError func(error)) error { + submitBlocksFn: func(ctx context.Context, headers []*types.SignedHeader, data []*types.Data, cacheMgr cache.Manager, signer signer.Signer, onSubmitError func(error)) error { return nil }, } @@ -62,32 +53,31 @@ func TestTracedDASubmitter_SubmitHeaders_Success(t *testing.T) { {Header: types.Header{BaseHeader: types.BaseHeader{Height: 101}}}, {Header: types.Header{BaseHeader: types.BaseHeader{Height: 102}}}, } - marshalledHeaders := [][]byte{ - []byte("header1"), - []byte("header2"), - []byte("header3"), + data := []*types.Data{ + {Metadata: &types.Metadata{Height: 100}}, + {Metadata: &types.Metadata{Height: 101}}, + {Metadata: &types.Metadata{Height: 102}}, } - err := submitter.SubmitHeaders(ctx, headers, marshalledHeaders, nil, nil, nil) + err := submitter.SubmitBlocks(ctx, headers, data, nil, nil, nil) require.NoError(t, err) spans := sr.Ended() require.Len(t, spans, 1) span := spans[0] - require.Equal(t, "DASubmitter.SubmitHeaders", span.Name()) + require.Equal(t, "DASubmitter.SubmitBlocks", span.Name()) require.Equal(t, codes.Unset, span.Status().Code) attrs := span.Attributes() - testutil.RequireAttribute(t, attrs, "header.count", 3) - testutil.RequireAttribute(t, attrs, "header.total_bytes", 21) - testutil.RequireAttribute(t, attrs, "header.start_height", int64(100)) - testutil.RequireAttribute(t, attrs, "header.end_height", int64(102)) + testutil.RequireAttribute(t, attrs, "block.count", 3) + testutil.RequireAttribute(t, attrs, "block.start_height", int64(100)) + testutil.RequireAttribute(t, attrs, "block.end_height", int64(102)) } -func TestTracedDASubmitter_SubmitHeaders_Error(t *testing.T) { +func TestTracedDASubmitter_SubmitBlocks_Error(t *testing.T) { expectedErr := errors.New("DA submission failed") mock := &mockDASubmitterAPI{ - submitHeadersFn: func(ctx context.Context, headers []*types.SignedHeader, marshalledHeaders [][]byte, cache cache.Manager, signer signer.Signer, onSubmitError func(error)) error { + submitBlocksFn: func(ctx context.Context, headers []*types.SignedHeader, data []*types.Data, cacheMgr cache.Manager, signer signer.Signer, onSubmitError func(error)) error { return expectedErr }, } @@ -97,9 +87,11 @@ func TestTracedDASubmitter_SubmitHeaders_Error(t *testing.T) { headers := []*types.SignedHeader{ {Header: types.Header{BaseHeader: types.BaseHeader{Height: 100}}}, } - marshalledHeaders := [][]byte{[]byte("header1")} + data := []*types.Data{ + {Metadata: &types.Metadata{Height: 100}}, + } - err := submitter.SubmitHeaders(ctx, headers, marshalledHeaders, nil, nil, nil) + err := submitter.SubmitBlocks(ctx, headers, data, nil, nil, nil) require.Error(t, err) require.Equal(t, expectedErr, err) @@ -110,83 +102,22 @@ func TestTracedDASubmitter_SubmitHeaders_Error(t *testing.T) { require.Equal(t, expectedErr.Error(), span.Status().Description) } -func TestTracedDASubmitter_SubmitHeaders_Empty(t *testing.T) { - mock := &mockDASubmitterAPI{ - submitHeadersFn: func(ctx context.Context, headers []*types.SignedHeader, marshalledHeaders [][]byte, cache cache.Manager, signer signer.Signer, onSubmitError func(error)) error { - return nil - }, - } - submitter, sr := setupDASubmitterTrace(t, mock) - ctx := context.Background() - - err := submitter.SubmitHeaders(ctx, []*types.SignedHeader{}, [][]byte{}, nil, nil, nil) - require.NoError(t, err) - - spans := sr.Ended() - require.Len(t, spans, 1) - span := spans[0] - - attrs := span.Attributes() - testutil.RequireAttribute(t, attrs, "header.count", 0) - testutil.RequireAttribute(t, attrs, "header.total_bytes", 0) -} - -func TestTracedDASubmitter_SubmitData_Success(t *testing.T) { +func TestTracedDASubmitter_SubmitBlocks_Empty(t *testing.T) { mock := &mockDASubmitterAPI{ - submitDataFn: func(ctx context.Context, signedDataList []*types.SignedData, marshalledData [][]byte, cache cache.Manager, signer signer.Signer, genesis genesis.Genesis, onSubmitError func(error)) error { + submitBlocksFn: func(ctx context.Context, headers []*types.SignedHeader, data []*types.Data, cacheMgr cache.Manager, signer signer.Signer, onSubmitError func(error)) error { return nil }, } submitter, sr := setupDASubmitterTrace(t, mock) ctx := context.Background() - signedDataList := []*types.SignedData{ - {Data: types.Data{Metadata: &types.Metadata{Height: 100}}}, - {Data: types.Data{Metadata: &types.Metadata{Height: 101}}}, - } - marshalledData := [][]byte{ - []byte("data1data1"), - []byte("data2data2"), - } - - err := submitter.SubmitData(ctx, signedDataList, marshalledData, nil, nil, genesis.Genesis{}, nil) + err := submitter.SubmitBlocks(ctx, []*types.SignedHeader{}, []*types.Data{}, nil, nil, nil) require.NoError(t, err) spans := sr.Ended() require.Len(t, spans, 1) span := spans[0] - require.Equal(t, "DASubmitter.SubmitData", span.Name()) - require.Equal(t, codes.Unset, span.Status().Code) attrs := span.Attributes() - testutil.RequireAttribute(t, attrs, "data.count", 2) - testutil.RequireAttribute(t, attrs, "data.total_bytes", 20) - testutil.RequireAttribute(t, attrs, "data.start_height", int64(100)) - testutil.RequireAttribute(t, attrs, "data.end_height", int64(101)) -} - -func TestTracedDASubmitter_SubmitData_Error(t *testing.T) { - expectedErr := errors.New("data submission failed") - mock := &mockDASubmitterAPI{ - submitDataFn: func(ctx context.Context, signedDataList []*types.SignedData, marshalledData [][]byte, cache cache.Manager, signer signer.Signer, genesis genesis.Genesis, onSubmitError func(error)) error { - return expectedErr - }, - } - submitter, sr := setupDASubmitterTrace(t, mock) - ctx := context.Background() - - signedDataList := []*types.SignedData{ - {Data: types.Data{Metadata: &types.Metadata{Height: 100}}}, - } - marshalledData := [][]byte{[]byte("data1")} - - err := submitter.SubmitData(ctx, signedDataList, marshalledData, nil, nil, genesis.Genesis{}, nil) - require.Error(t, err) - require.Equal(t, expectedErr, err) - - spans := sr.Ended() - require.Len(t, spans, 1) - span := spans[0] - require.Equal(t, codes.Error, span.Status().Code) - require.Equal(t, expectedErr.Error(), span.Status().Description) + testutil.RequireAttribute(t, attrs, "block.count", 0) } diff --git a/block/internal/submitting/submitter.go b/block/internal/submitting/submitter.go index 2c613327f7..92d0ad45de 100644 --- a/block/internal/submitting/submitter.go +++ b/block/internal/submitting/submitter.go @@ -25,8 +25,7 @@ import ( // DASubmitterAPI defines minimal methods needed by Submitter for DA submissions. type DASubmitterAPI interface { - SubmitHeaders(ctx context.Context, headers []*types.SignedHeader, marshalledHeaders [][]byte, cache cache.Manager, signer signer.Signer, onSubmitError func(error)) error - SubmitData(ctx context.Context, signedDataList []*types.SignedData, marshalledData [][]byte, cache cache.Manager, signer signer.Signer, genesis genesis.Genesis, onSubmitError func(error)) error + SubmitBlocks(ctx context.Context, headers []*types.SignedHeader, data []*types.Data, cache cache.Manager, signer signer.Signer, onSubmitError func(error)) error Close() } @@ -52,13 +51,11 @@ type Submitter struct { // DA state daIncludedHeight *atomic.Uint64 - // Submission state to prevent concurrent submissions - headerSubmissionMtx sync.Mutex - dataSubmissionMtx sync.Mutex + // Submission mutex to prevent concurrent submissions + submissionMtx sync.Mutex // Batching strategy state - lastHeaderSubmit atomic.Int64 // stores Unix nanoseconds - lastDataSubmit atomic.Int64 // stores Unix nanoseconds + lastSubmit atomic.Int64 // stores Unix nanoseconds batchingStrategy BatchingStrategy // Channels for coordination @@ -112,8 +109,7 @@ func NewSubmitter( } now := time.Now().UnixNano() - submitter.lastHeaderSubmit.Store(now) - submitter.lastDataSubmit.Store(now) + submitter.lastSubmit.Store(now) return submitter } @@ -170,7 +166,7 @@ func (s *Submitter) Stop() error { return nil } -// daSubmissionLoop handles submission of headers and data to DA layer (aggregator nodes only) +// daSubmissionLoop handles submission of blocks to DA layer (aggregator nodes only) func (s *Submitter) daSubmissionLoop() { s.logger.Info().Msg("starting DA submission loop") defer s.logger.Info().Msg("DA submission loop stopped") @@ -186,21 +182,18 @@ func (s *Submitter) daSubmissionLoop() { case <-s.ctx.Done(): return case <-ticker.C: - // Check if we should submit headers based on batching strategy - headersNb := s.cache.NumPendingHeaders() - if headersNb > 0 { - lastSubmitNanos := s.lastHeaderSubmit.Load() + pendingHeaders := s.cache.NumPendingHeaders() + if pendingHeaders > 0 { + lastSubmitNanos := s.lastSubmit.Load() timeSinceLastSubmit := time.Since(time.Unix(0, lastSubmitNanos)) // For strategy decision, we need to estimate the size // We'll fetch headers to check, but only submit if strategy approves - if s.headerSubmissionMtx.TryLock() { - s.logger.Debug().Time("t", time.Now()).Uint64("headers", headersNb).Msg("Header submission in progress") + if s.submissionMtx.TryLock() { s.wg.Add(1) go func() { defer func() { - s.headerSubmissionMtx.Unlock() - s.logger.Debug().Time("t", time.Now()).Uint64("headers", headersNb).Msg("Header submission completed") + s.submissionMtx.Unlock() s.wg.Done() }() @@ -210,14 +203,14 @@ func (s *Submitter) daSubmissionLoop() { if len(headers) > 0 { s.cache.ResetInFlightHeaderRange(headers[0].Height(), headers[len(headers)-1].Height()) } - s.logger.Error().Err(err).Msg("failed to get pending headers for batching decision") + s.logger.Error().Err(err).Msg("failed to get pending headers") return } if len(headers) == 0 { return } - // Calculate total size (excluding signature) + // Calculate total size totalSize := uint64(0) for _, marshalled := range marshalledHeaders { totalSize += uint64(len(marshalled)) @@ -237,18 +230,22 @@ func (s *Submitter) daSubmissionLoop() { return } - s.logger.Debug(). - Time("t", time.Now()). - Uint64("headers", headersNb). - Uint64("total_size_kb", totalSize/1024). - Dur("time_since_last", timeSinceLastSubmit). - Msg("batching strategy triggered header submission") + dataList := make([]*types.Data, len(headers)) + for i, hdr := range headers { + _, data, fetchErr := s.store.GetBlockData(s.ctx, hdr.Height()) + if fetchErr != nil { + s.cache.ResetInFlightHeaderRange(headers[0].Height(), headers[len(headers)-1].Height()) + s.logger.Error().Err(fetchErr).Msg("failed to get block data for pending header") + return + } + dataList[i] = data + } - s.lastHeaderSubmit.Store(time.Now().UnixNano()) + s.lastSubmit.Store(time.Now().UnixNano()) onError := func(err error) { if errors.Is(err, common.ErrOversizedItem) { s.logger.Error().Err(err). - Msg("CRITICAL: Header exceeds DA blob size limit - halting to prevent live lock") + Msg("CRITICAL: Block exceeds DA blob size limit - halting to prevent live lock") s.sendCriticalError(fmt.Errorf("unrecoverable DA submission error: %w", err)) return } @@ -256,101 +253,21 @@ func (s *Submitter) daSubmissionLoop() { s.cache.ResetInFlightHeaderRange(headers[0].Height(), headers[len(headers)-1].Height()) } if err != nil { - s.logger.Error().Err(err).Msg("failed to submit headers") + s.logger.Error().Err(err).Msg("failed to submit blocks") } } - if err := s.daSubmitter.SubmitHeaders(s.ctx, headers, marshalledHeaders, s.cache, s.signer, onError); err != nil { + if err := s.daSubmitter.SubmitBlocks(s.ctx, headers, dataList, s.cache, s.signer, onError); err != nil { if len(headers) > 0 { s.cache.ResetInFlightHeaderRange(headers[0].Height(), headers[len(headers)-1].Height()) } - s.logger.Error().Err(err).Msg("failed to enqueue header submission") - } - }() - } - } - - // Check if we should submit data based on batching strategy - dataNb := s.cache.NumPendingData() - if dataNb > 0 { - lastSubmitNanos := s.lastDataSubmit.Load() - timeSinceLastSubmit := time.Since(time.Unix(0, lastSubmitNanos)) - if s.dataSubmissionMtx.TryLock() { - s.logger.Debug().Time("t", time.Now()).Uint64("data", dataNb).Msg("Data submission in progress") - s.wg.Add(1) - go func() { - defer func() { - s.dataSubmissionMtx.Unlock() - s.logger.Debug().Time("t", time.Now()).Uint64("data", dataNb).Msg("Data submission completed") - s.wg.Done() - }() - - // Get data with marshalled bytes from cache - signedDataList, marshalledData, err := s.cache.GetPendingData(s.ctx) - if err != nil { - if len(signedDataList) > 0 { - s.cache.ResetInFlightDataRange(signedDataList[0].Height(), signedDataList[len(signedDataList)-1].Height()) - } - s.logger.Error().Err(err).Msg("failed to get pending data for batching decision") - return - } - if len(signedDataList) == 0 { - return - } - - // Calculate total size (excluding signature) - totalSize := uint64(0) - for _, marshalled := range marshalledData { - totalSize += uint64(len(marshalled)) - } - - shouldSubmit := s.batchingStrategy.ShouldSubmit( - uint64(len(signedDataList)), - totalSize, - common.DefaultMaxBlobSize, - timeSinceLastSubmit, - ) - - if !shouldSubmit { - if len(signedDataList) > 0 { - s.cache.ResetInFlightDataRange(signedDataList[0].Height(), signedDataList[len(signedDataList)-1].Height()) - } - return - } - - s.logger.Debug(). - Time("t", time.Now()). - Uint64("data", dataNb). - Uint64("total_size_kb", totalSize/1024). - Dur("time_since_last", timeSinceLastSubmit). - Msg("batching strategy triggered data submission") - - s.lastDataSubmit.Store(time.Now().UnixNano()) - onError := func(err error) { - if errors.Is(err, common.ErrOversizedItem) { - s.logger.Error().Err(err). - Msg("CRITICAL: Data exceeds DA blob size limit - halting to prevent live lock") - s.sendCriticalError(fmt.Errorf("unrecoverable DA submission error: %w", err)) - return - } - if len(signedDataList) > 0 { - s.cache.ResetInFlightDataRange(signedDataList[0].Height(), signedDataList[len(signedDataList)-1].Height()) - } - if err != nil { - s.logger.Error().Err(err).Msg("failed to submit data") - } - } - if err := s.daSubmitter.SubmitData(s.ctx, signedDataList, marshalledData, s.cache, s.signer, s.genesis, onError); err != nil { - if len(signedDataList) > 0 { - s.cache.ResetInFlightDataRange(signedDataList[0].Height(), signedDataList[len(signedDataList)-1].Height()) - } - s.logger.Error().Err(err).Msg("failed to enqueue data submission") + s.logger.Error().Err(err).Msg("failed to enqueue block submission") } }() } } // Update metrics with current pending counts - s.metrics.DASubmitterPendingBlobs.Set(float64(headersNb + dataNb)) + s.metrics.DASubmitterPendingBlobs.Set(float64(pendingHeaders)) } } } @@ -489,8 +406,6 @@ func (s *Submitter) sendCriticalError(err error) { // setNodeHeightToDAHeight persists the DA heights for a block's header and data. // For empty-tx blocks, both use the header DA height since no data blob is posted. func (s *Submitter) setNodeHeightToDAHeight(ctx context.Context, height uint64, data *types.Data, genesisInclusion bool) error { - dataHash := data.DACommitment() - daHeightForHeader, ok := s.cache.GetHeaderDAIncludedByHeight(height) if !ok { return fmt.Errorf("header for height %d not found in cache", height) @@ -501,8 +416,9 @@ func (s *Submitter) setNodeHeightToDAHeight(ctx context.Context, height uint64, } genesisDAIncludedHeight := daHeightForHeader - // For empty transactions, use the same DA height as the header. + // For empty transactions, use the same DA height as the header dataDAHeight := daHeightForHeader + dataHash := data.DACommitment() if !bytes.Equal(dataHash, common.DataHashForEmptyTxs) { daHeightForData, ok := s.cache.GetDataDAIncludedByHeight(height) if !ok { @@ -546,11 +462,10 @@ func (s *Submitter) IsHeightDAIncluded(height uint64, data *types.Data) (bool, e return false, nil } - dataCommitment := data.DACommitment() - _, headerIncluded := s.cache.GetHeaderDAIncludedByHeight(height) _, dataIncluded := s.cache.GetDataDAIncludedByHeight(height) + dataCommitment := data.DACommitment() dataIncluded = bytes.Equal(dataCommitment, common.DataHashForEmptyTxs) || dataIncluded return headerIncluded && dataIncluded, nil diff --git a/block/internal/submitting/submitter_test.go b/block/internal/submitting/submitter_test.go index 6ae51a7ef5..3d03fb0caa 100644 --- a/block/internal/submitting/submitter_test.go +++ b/block/internal/submitting/submitter_test.go @@ -175,7 +175,7 @@ func TestSubmitter_setSequencerHeightToDAHeight(t *testing.T) { daClient.On("GetDataNamespace").Return([]byte(cfg.DA.DataNamespace)).Maybe() daClient.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() daClient.On("HasForcedInclusionNamespace").Return(false).Maybe() - daSub := NewDASubmitter(daClient, cfg, genesis.Genesis{}, common.BlockOptions{}, metrics, zerolog.Nop(), nil, nil) + daSub := NewDASubmitter(daClient, cfg, genesis.Genesis{}, common.BlockOptions{}, metrics, zerolog.Nop()) s := NewSubmitter(mockStore, nil, cm, metrics, cfg, genesis.Genesis{}, daSub, nil, nil, zerolog.Nop(), nil) s.ctx = ctx @@ -258,7 +258,7 @@ func TestSubmitter_processDAInclusionLoop_advances(t *testing.T) { daClient.On("GetDataNamespace").Return([]byte(cfg.DA.DataNamespace)).Maybe() daClient.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() daClient.On("HasForcedInclusionNamespace").Return(false).Maybe() - daSub := NewDASubmitter(daClient, cfg, genesis.Genesis{}, common.BlockOptions{}, metrics, zerolog.Nop(), nil, nil) + daSub := NewDASubmitter(daClient, cfg, genesis.Genesis{}, common.BlockOptions{}, metrics, zerolog.Nop()) s := NewSubmitter(st, exec, cm, metrics, cfg, genesis.Genesis{}, daSub, nil, nil, zerolog.Nop(), nil) // prepare two consecutive blocks in store with DA included in cache @@ -350,8 +350,7 @@ func TestSubmitter_daSubmissionLoop(t *testing.T) { // Prepare fake DA submitter capturing calls fakeDA := &fakeDASubmitter{ - chHdr: make(chan struct{}, 1), - chData: make(chan struct{}, 1), + chBlocks: make(chan struct{}, 1), } // Provide a non-nil executor; it won't be used because DA inclusion won't advance @@ -376,8 +375,7 @@ func TestSubmitter_daSubmissionLoop(t *testing.T) { // Set last submit times far in past so strategy allows submission pastTime := time.Now().Add(-time.Hour).UnixNano() - s.lastHeaderSubmit.Store(pastTime) - s.lastDataSubmit.Store(pastTime) + s.lastSubmit.Store(pastTime) // Make there be pending headers and data by setting store height > last submitted h1, d1 := newHeaderAndData("test-chain", 1, true) @@ -398,16 +396,7 @@ func TestSubmitter_daSubmissionLoop(t *testing.T) { // Both should be invoked eventually require.Eventually(t, func() bool { select { - case <-fakeDA.chHdr: - return true - default: - return false - } - }, time.Second, 10*time.Millisecond) - - require.Eventually(t, func() bool { - select { - case <-fakeDA.chData: + case <-fakeDA.chBlocks: return true default: return false @@ -417,21 +406,12 @@ func TestSubmitter_daSubmissionLoop(t *testing.T) { // fakeDASubmitter is a lightweight test double for the DASubmitter used by the loop. type fakeDASubmitter struct { - chHdr chan struct{} - chData chan struct{} -} - -func (f *fakeDASubmitter) SubmitHeaders(ctx context.Context, _ []*types.SignedHeader, _ [][]byte, _ cache.Manager, _ signer.Signer, _ func(error)) error { - select { - case f.chHdr <- struct{}{}: - default: - } - return nil + chBlocks chan struct{} } -func (f *fakeDASubmitter) SubmitData(ctx context.Context, _ []*types.SignedData, _ [][]byte, _ cache.Manager, _ signer.Signer, _ genesis.Genesis, _ func(error)) error { +func (f *fakeDASubmitter) SubmitBlocks(ctx context.Context, _ []*types.SignedHeader, _ []*types.Data, _ cache.Manager, _ signer.Signer, _ func(error)) error { select { - case f.chData <- struct{}{}: + case f.chBlocks <- struct{}{}: default: } return nil @@ -466,7 +446,7 @@ func TestSubmitter_CacheClearedOnHeightInclusion(t *testing.T) { daClient.On("GetDataNamespace").Return([]byte(cfg.DA.DataNamespace)).Maybe() daClient.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() daClient.On("HasForcedInclusionNamespace").Return(false).Maybe() - daSub := NewDASubmitter(daClient, cfg, genesis.Genesis{}, common.BlockOptions{}, metrics, zerolog.Nop(), nil, nil) + daSub := NewDASubmitter(daClient, cfg, genesis.Genesis{}, common.BlockOptions{}, metrics, zerolog.Nop()) s := NewSubmitter(st, exec, cm, metrics, cfg, genesis.Genesis{}, daSub, nil, nil, zerolog.Nop(), nil) // Create test blocks diff --git a/block/internal/syncing/da_follower.go b/block/internal/syncing/da_follower.go index 443fb876ae..9a4a97c730 100644 --- a/block/internal/syncing/da_follower.go +++ b/block/internal/syncing/da_follower.go @@ -3,8 +3,6 @@ package syncing import ( "context" "errors" - "slices" - "sync" "time" "github.com/rs/zerolog" @@ -20,8 +18,6 @@ type DAFollower interface { Start(ctx context.Context) error Stop() HasReachedHead() bool - // QueuePriorityHeight queues a DA height for priority retrieval (from P2P hints). - QueuePriorityHeight(daHeight uint64) } // daFollower is the concrete implementation of DAFollower. @@ -30,24 +26,6 @@ type daFollower struct { retriever DARetriever eventSink common.EventSink logger zerolog.Logger - - // Priority queue for P2P hint heights (absorbed from DARetriever refactoring #2). - priorityMu sync.Mutex - priorityHeights []uint64 -} - -const maxPriorityHeights = 1024 - -// DAFollowerConfig holds configuration for creating a DAFollower. -type DAFollowerConfig struct { - Client da.Client - Retriever DARetriever - Logger zerolog.Logger - EventSink common.EventSink - Namespace []byte - DataNamespace []byte // may be nil or equal to Namespace - StartDAHeight uint64 - DABlockTime time.Duration } // NewDAFollower creates a new daFollower. @@ -58,10 +36,9 @@ func NewDAFollower(cfg DAFollowerConfig) DAFollower { } f := &daFollower{ - retriever: cfg.Retriever, - eventSink: cfg.EventSink, - logger: cfg.Logger.With().Str("component", "da_follower").Logger(), - priorityHeights: make([]uint64, 0), + retriever: cfg.Retriever, + eventSink: cfg.EventSink, + logger: cfg.Logger.With().Str("component", "da_follower").Logger(), } f.subscriber = da.NewSubscriber(da.SubscriberConfig{ @@ -76,6 +53,18 @@ func NewDAFollower(cfg DAFollowerConfig) DAFollower { return f } +// DAFollowerConfig holds configuration for creating a DAFollower. +type DAFollowerConfig struct { + Client da.Client + Retriever DARetriever + Logger zerolog.Logger + EventSink common.EventSink + Namespace []byte + DataNamespace []byte // may be nil or equal to Namespace + StartDAHeight uint64 + DABlockTime time.Duration +} + // Start begins the follow and catchup goroutines. func (f *daFollower) Start(ctx context.Context) error { return f.subscriber.Start(ctx) @@ -105,7 +94,7 @@ func (f *daFollower) HandleEvent(ctx context.Context, ev datypes.SubscriptionEve events := f.retriever.ProcessBlobs(ctx, ev.Blobs, ev.Height) if len(events) == 0 { - return errors.New("skip inline: no complete events") // Split namespace, subscriber rolls back + return errors.New("skip inline: no complete events") } for _, event := range events { @@ -121,44 +110,8 @@ func (f *daFollower) HandleEvent(ctx context.Context, ev datypes.SubscriptionEve return nil } -// HandleCatchup retrieves events at a single DA height and pipes them -// to the event sink. Checks priority heights first. +// HandleCatchup retrieves events at a single DA height and pipes them to the event sink. func (f *daFollower) HandleCatchup(ctx context.Context, daHeight uint64) error { - // 1. Drain stale or future priority heights from P2P hints - for priorityHeight := f.popPriorityHeight(); priorityHeight != 0; priorityHeight = f.popPriorityHeight() { - if priorityHeight < daHeight { - continue // skip stale hints without yielding back to the catchup loop - } - - f.logger.Debug(). - Uint64("da_height", priorityHeight). - Msg("fetching priority DA height from P2P hint") - - if err := f.fetchAndPipeHeight(ctx, priorityHeight); err != nil { - if errors.Is(err, datypes.ErrHeightFromFuture) { - // Priority hint points to a future height — silently ignore. - f.logger.Debug().Uint64("priority_da_height", priorityHeight). - Msg("priority hint is from future, ignoring") - continue - } - // Roll back so daHeight is attempted again next cycle after backoff. - return err - } - break // continue with daHeight - } - - // 2. Normal sequential fetch - if err := f.fetchAndPipeHeight(ctx, daHeight); err != nil { - return err - } - return nil -} - -// fetchAndPipeHeight retrieves events at a single DA height and pipes them. -// It does NOT handle ErrHeightFromFuture — callers must decide how to react -// because the correct response depends on whether this is a normal sequential -// catchup or a priority-hint fetch. -func (f *daFollower) fetchAndPipeHeight(ctx context.Context, daHeight uint64) error { events, err := f.retriever.RetrieveFromDA(ctx, daHeight) if err != nil { if errors.Is(err, datypes.ErrBlobNotFound) { @@ -175,41 +128,3 @@ func (f *daFollower) fetchAndPipeHeight(ctx context.Context, daHeight uint64) er return nil } - -// QueuePriorityHeight queues a DA height for priority retrieval. -func (f *daFollower) QueuePriorityHeight(daHeight uint64) { - f.priorityMu.Lock() - defer f.priorityMu.Unlock() - - idx, found := slices.BinarySearch(f.priorityHeights, daHeight) - if found { - return - } - - // Keep the queue bounded. When full, prefer lower (sooner) heights. - if len(f.priorityHeights) >= maxPriorityHeights { - last := f.priorityHeights[len(f.priorityHeights)-1] - if daHeight >= last { - return - } - f.priorityHeights = f.priorityHeights[:len(f.priorityHeights)-1] - } - - f.priorityHeights = slices.Insert(f.priorityHeights, idx, daHeight) -} - -// popPriorityHeight returns the next priority height to fetch, or 0 if none. -func (f *daFollower) popPriorityHeight() uint64 { - f.priorityMu.Lock() - defer f.priorityMu.Unlock() - - if len(f.priorityHeights) == 0 { - return 0 - } - height := f.priorityHeights[0] - f.priorityHeights = f.priorityHeights[1:] - if len(f.priorityHeights) == 0 { - f.priorityHeights = nil - } - return height -} diff --git a/block/internal/syncing/da_follower_test.go b/block/internal/syncing/da_follower_test.go index 710d2d81d0..58ac9c3967 100644 --- a/block/internal/syncing/da_follower_test.go +++ b/block/internal/syncing/da_follower_test.go @@ -97,13 +97,11 @@ func TestDAFollower_HandleEvent(t *testing.T) { func TestDAFollower_HandleCatchup(t *testing.T) { type spec struct { - daHeight uint64 - initialPriorityHeights []uint64 - pipeErr error - setupMock func(m *MockDARetriever) - wantErrIs error - wantPipedHeights []uint64 - wantRemainingPriority []uint64 + daHeight uint64 + pipeErr error + setupMock func(m *MockDARetriever) + wantErrIs error + wantPipedHeights []uint64 } newFollower := func(t *testing.T, s spec, m *MockDARetriever) (*daFollower, func() []common.DAHeightEvent) { @@ -116,10 +114,9 @@ func TestDAFollower_HandleCatchup(t *testing.T) { } follower := &daFollower{ - retriever: m, - eventSink: common.EventSinkFunc(pipeEvent), - logger: zerolog.Nop(), - priorityHeights: append([]uint64(nil), s.initialPriorityHeights...), + retriever: m, + eventSink: common.EventSinkFunc(pipeEvent), + logger: zerolog.Nop(), } return follower, func() []common.DAHeightEvent { return pipedEvents } } @@ -148,27 +145,6 @@ func TestDAFollower_HandleCatchup(t *testing.T) { Return(nil, datypes.ErrHeightFromFuture).Once() }, }, - "prio_first": { - daHeight: 100, - initialPriorityHeights: []uint64{105}, - wantPipedHeights: []uint64{105, 100}, - setupMock: func(m *MockDARetriever) { - m.On("RetrieveFromDA", mock.Anything, uint64(105)). - Return([]common.DAHeightEvent{{DaHeight: 105}}, nil).Once() - m.On("RetrieveFromDA", mock.Anything, uint64(100)). - Return([]common.DAHeightEvent{{DaHeight: 100}}, nil).Once() - }, - }, - "skip_stale_prio_already_included": { - daHeight: 100, - initialPriorityHeights: []uint64{99}, - wantPipedHeights: []uint64{100}, - setupMock: func(m *MockDARetriever) { - // stale priority hint (< daHeight) is discarded; only sequential height is fetched - m.On("RetrieveFromDA", mock.Anything, uint64(100)). - Return([]common.DAHeightEvent{{DaHeight: 100}}, nil).Once() - }, - }, } for name, s := range specs { @@ -197,57 +173,6 @@ func TestDAFollower_HandleCatchup(t *testing.T) { wantHeights = []uint64{} } assert.Equal(t, wantHeights, gotHeights) - - if s.wantRemainingPriority != nil { - assert.Equal(t, s.wantRemainingPriority, follower.priorityHeights) - } else { - assert.Empty(t, follower.priorityHeights) - } - }) - } -} - -func TestDAFollower_QueuePriorityHeight(t *testing.T) { - specs := map[string]struct { - initial []uint64 - queue []uint64 - want []uint64 - }{ - "sorts_and_deduplicates": { - initial: []uint64{5, 10}, - queue: []uint64{7, 10, 3}, - want: []uint64{3, 5, 7, 10}, - }, - "bounded_drops_largest_when_smaller_arrives": { - initial: makeRange(1, maxPriorityHeights), - queue: []uint64{maxPriorityHeights + 1, 0}, - want: append([]uint64{0}, makeRange(1, maxPriorityHeights-1)...), - }, - } - - for name, spec := range specs { - t.Run(name, func(t *testing.T) { - follower := &daFollower{ - logger: zerolog.Nop(), - priorityHeights: append([]uint64(nil), spec.initial...), - } - - for _, daHeight := range spec.queue { - follower.QueuePriorityHeight(daHeight) - } - - assert.Equal(t, spec.want, follower.priorityHeights) }) } } - -func makeRange(start, end uint64) []uint64 { - if end < start { - return nil - } - out := make([]uint64, 0, end-start+1) - for v := start; v <= end; v++ { - out = append(out, v) - } - return out -} diff --git a/block/internal/syncing/da_retriever.go b/block/internal/syncing/da_retriever.go index d4fa93ce04..18b6833899 100644 --- a/block/internal/syncing/da_retriever.go +++ b/block/internal/syncing/da_retriever.go @@ -1,22 +1,16 @@ package syncing import ( - "bytes" "context" - "errors" "fmt" - "sync" "github.com/rs/zerolog" - "google.golang.org/protobuf/proto" "github.com/evstack/ev-node/block/internal/cache" "github.com/evstack/ev-node/block/internal/common" "github.com/evstack/ev-node/block/internal/da" datypes "github.com/evstack/ev-node/pkg/da/types" "github.com/evstack/ev-node/pkg/genesis" - "github.com/evstack/ev-node/types" - pb "github.com/evstack/ev-node/types/pb/evnode/v1" ) // DARetriever defines the interface for retrieving events from the DA layer @@ -34,16 +28,6 @@ type daRetriever struct { cache cache.CacheManager genesis genesis.Genesis logger zerolog.Logger - - mu sync.Mutex - // transient cache, only full event need to be passed to the syncer - // on restart, will be refetch as da height is updated by syncer - pendingHeaders map[uint64]*types.SignedHeader - pendingData map[uint64]*types.Data - - // strictMode indicates if the node has seen a valid DAHeaderEnvelope - // and should now reject all legacy/unsigned headers. - strictMode bool } // NewDARetriever creates a new DA retriever @@ -54,13 +38,10 @@ func NewDARetriever( logger zerolog.Logger, ) *daRetriever { return &daRetriever{ - client: client, - cache: cache, - genesis: genesis, - logger: logger.With().Str("component", "da_retriever").Logger(), - pendingHeaders: make(map[uint64]*types.SignedHeader), - pendingData: make(map[uint64]*types.Data), - strictMode: false, + client: client, + cache: cache, + genesis: genesis, + logger: logger.With().Str("component", "da_retriever").Logger(), } } @@ -78,153 +59,97 @@ func (r *daRetriever) RetrieveFromDA(ctx context.Context, daHeight uint64) ([]co } r.logger.Debug().Int("blobs", len(blobsResp.Data)).Uint64("da_height", daHeight).Msg("retrieved blob data") - return r.processBlobs(ctx, blobsResp.Data, daHeight), nil + return r.processBlobs(blobsResp.Data, daHeight), nil } -// fetchBlobs retrieves blobs from both header and data namespaces +// fetchBlobs retrieves blobs from the DA layer at the specified height func (r *daRetriever) fetchBlobs(ctx context.Context, daHeight uint64) (datypes.ResultRetrieve, error) { - // Retrieve from both namespaces using the DA client - headerRes := r.client.RetrieveBlobs(ctx, daHeight, r.client.GetHeaderNamespace()) - - // If namespaces are the same, return header result - if bytes.Equal(r.client.GetHeaderNamespace(), r.client.GetDataNamespace()) { - return headerRes, r.validateBlobResponse(headerRes, daHeight) - } - - dataRes := r.client.RetrieveBlobs(ctx, daHeight, r.client.GetDataNamespace()) - - // Validate responses - headerErr := r.validateBlobResponse(headerRes, daHeight) - // ignoring error not found, as data can have data - if headerErr != nil && !errors.Is(headerErr, datypes.ErrBlobNotFound) { - return headerRes, headerErr - } - - dataErr := r.validateBlobResponse(dataRes, daHeight) - // ignoring error not found, as header can have data - if dataErr != nil && !errors.Is(dataErr, datypes.ErrBlobNotFound) { - return dataRes, dataErr - } - - // Combine successful results - combinedResult := datypes.ResultRetrieve{ - BaseResult: datypes.BaseResult{ - Code: datypes.StatusSuccess, - Height: daHeight, - }, - Data: make([][]byte, 0), - } - - if headerRes.Code == datypes.StatusSuccess { - combinedResult.Data = append(combinedResult.Data, headerRes.Data...) - combinedResult.IDs = append(combinedResult.IDs, headerRes.IDs...) - } - - if dataRes.Code == datypes.StatusSuccess { - combinedResult.Data = append(combinedResult.Data, dataRes.Data...) - combinedResult.IDs = append(combinedResult.IDs, dataRes.IDs...) - } - - // Re-throw error not found if both were not found. - if len(combinedResult.Data) == 0 && len(combinedResult.IDs) == 0 { - r.logger.Debug().Uint64("da_height", daHeight).Msg("no blob data found") - combinedResult.Code = datypes.StatusNotFound - combinedResult.Message = datypes.ErrBlobNotFound.Error() - return combinedResult, datypes.ErrBlobNotFound - } - - return combinedResult, nil -} + res := r.client.RetrieveBlobs(ctx, daHeight, r.client.GetHeaderNamespace()) -// validateBlobResponse validates a blob response from DA layer -// those are the only error code returned by da.RetrieveWithHelpers -func (r *daRetriever) validateBlobResponse(res datypes.ResultRetrieve, daHeight uint64) error { switch res.Code { case datypes.StatusError: - return fmt.Errorf("DA retrieval failed: %s", res.Message) + return res, fmt.Errorf("DA retrieval failed: %s", res.Message) case datypes.StatusHeightFromFuture: - return fmt.Errorf("%w: height from future", datypes.ErrHeightFromFuture) + return res, fmt.Errorf("%w: height from future", datypes.ErrHeightFromFuture) case datypes.StatusNotFound: - return fmt.Errorf("%w: blob not found", datypes.ErrBlobNotFound) + return res, fmt.Errorf("%w: blob not found", datypes.ErrBlobNotFound) case datypes.StatusSuccess: r.logger.Debug().Uint64("da_height", daHeight).Msg("successfully retrieved from DA") - return nil + return res, nil default: - return nil + return res, nil } } // ProcessBlobs processes raw blob bytes to extract headers and data and returns height events. // This is the public interface used by the DAFollower for inline subscription processing. -func (r *daRetriever) ProcessBlobs(ctx context.Context, blobs [][]byte, daHeight uint64) []common.DAHeightEvent { - return r.processBlobs(ctx, blobs, daHeight) +func (r *daRetriever) ProcessBlobs(_ context.Context, blobs [][]byte, daHeight uint64) []common.DAHeightEvent { + return r.processBlobs(blobs, daHeight) } // processBlobs processes retrieved blobs to extract headers and data and returns height events -func (r *daRetriever) processBlobs(ctx context.Context, blobs [][]byte, daHeight uint64) []common.DAHeightEvent { - r.mu.Lock() - defer r.mu.Unlock() +func (r *daRetriever) processBlobs(blobs [][]byte, daHeight uint64) []common.DAHeightEvent { + var events []common.DAHeightEvent - // Decode all blobs for _, bz := range blobs { if len(bz) == 0 { continue } - if header := r.tryDecodeHeader(bz, daHeight); header != nil { - if _, ok := r.pendingHeaders[header.Height()]; ok { - // a (malicious) node may have re-published valid header to another da height (should never happen) - // we can already discard it, only the first one is valid - r.logger.Debug().Uint64("height", header.Height()).Uint64("da_height", daHeight).Msg("header blob already exists for height, discarding") - continue - } + header, data, envelopeSig, err := common.UnmarshalBlockBlob(bz) + if err != nil { + r.logger.Debug().Err(err).Msg("failed to decode combined block blob, skipping") + continue + } - r.pendingHeaders[header.Height()] = header + if err := header.Header.ValidateBasic(); err != nil { + r.logger.Debug().Err(err).Msg("invalid header structure") continue } - if data := r.tryDecodeData(bz, daHeight); data != nil { - if _, ok := r.pendingData[data.Height()]; ok { - // a (malicious) node may have re-published valid data to another da height (should never happen) - // we can already discard it, only the first one is valid - r.logger.Debug().Uint64("height", data.Height()).Uint64("da_height", daHeight).Msg("data blob already exists for height, discarding") + if err := r.assertExpectedProposer(header.ProposerAddress); err != nil { + r.logger.Debug().Err(err).Msg("unexpected proposer") + continue + } + + if len(envelopeSig) > 0 { + if header.Signer.PubKey == nil { + r.logger.Debug().Msg("header signer has no pubkey, cannot verify envelope") continue } - - r.pendingData[data.Height()] = data + payload, err := header.MarshalBinary() + if err != nil { + r.logger.Debug().Err(err).Msg("failed to marshal header for verification") + continue + } + if valid, err := header.Signer.PubKey.Verify(payload, envelopeSig); err != nil || !valid { + r.logger.Info().Err(err).Msg("DA envelope signature verification failed") + continue + } + r.logger.Debug().Uint64("height", header.Height()).Msg("DA envelope signature verified") } - } - - var events []common.DAHeightEvent - // Match headers with data and create events - for height, header := range r.pendingHeaders { - data := r.pendingData[height] + // Update cache with DA inclusion information + headerHash := header.MemoizeHash().String() + r.cache.SetHeaderDAIncluded(headerHash, daHeight, header.Height()) - // Handle empty data case - if data == nil { - if isEmptyDataExpected(header) { - data = createEmptyDataForHeader(ctx, header) - delete(r.pendingHeaders, height) - } else { - // keep header in pending headers until data lands - r.logger.Debug().Uint64("height", height).Msg("header found but no matching data") - continue - } - } else { - delete(r.pendingHeaders, height) - delete(r.pendingData, height) + if len(data.Txs) > 0 { + dataHash := data.DACommitment().String() + r.cache.SetDataDAIncluded(dataHash, daHeight, data.Height()) } - // Create height event - event := common.DAHeightEvent{ + r.logger.Debug(). + Str("header_hash", headerHash). + Uint64("da_height", daHeight). + Uint64("height", header.Height()). + Msg("decoded combined block blob") + + events = append(events, common.DAHeightEvent{ Header: header, Data: data, DaHeight: daHeight, Source: common.SourceDA, - } - - events = append(events, event) + }) } if len(events) > 0 { @@ -245,140 +170,7 @@ func (r *daRetriever) processBlobs(ctx context.Context, blobs [][]byte, daHeight return events } -// tryDecodeHeader attempts to decode a blob as a header -func (r *daRetriever) tryDecodeHeader(bz []byte, daHeight uint64) *types.SignedHeader { - header := new(types.SignedHeader) - - isValidEnvelope := false - - // Attempt to unmarshal as DAHeaderEnvelope and get the envelope signature - if envelopeSignature, err := header.UnmarshalDAEnvelope(bz); err != nil { - // If in strict mode, we REQUIRE an envelope. - if r.strictMode { - r.logger.Warn().Err(err).Msg("strict mode is enabled, rejecting non-envelope blob") - return nil - } - - // Fallback for backward compatibility (only if NOT in strict mode) - r.logger.Debug().Msg("trying legacy decoding") - var headerPb pb.SignedHeader - if errLegacy := proto.Unmarshal(bz, &headerPb); errLegacy != nil { - return nil - } - if errLegacy := header.FromProto(&headerPb); errLegacy != nil { - return nil - } - } else { - // We have a structurally valid envelope (or at least it parsed) - if len(envelopeSignature) > 0 { - if header.Signer.PubKey == nil { - r.logger.Debug().Msg("header signer has no pubkey, cannot verify envelope") - return nil - } - payload, err := header.MarshalBinary() - if err != nil { - r.logger.Debug().Err(err).Msg("failed to marshal header for verification") - return nil - } - if valid, err := header.Signer.PubKey.Verify(payload, envelopeSignature); err != nil || !valid { - r.logger.Info().Err(err).Msg("DA envelope signature verification failed") - return nil - } - r.logger.Debug().Uint64("height", header.Height()).Msg("DA envelope signature verified") - isValidEnvelope = true - } - } - if r.strictMode && !isValidEnvelope { - // no need to print warnings, as tryDecodeHeader could try to decode data first, which will show this warning. - r.logger.Debug().Msg("strict mode: rejecting block that is not a fully valid envelope") - return nil - } - - if err := header.Header.ValidateBasic(); err != nil { - r.logger.Debug().Err(err).Msg("invalid header structure") - return nil - } - - if err := r.assertExpectedProposer(header.ProposerAddress); err != nil { - r.logger.Debug().Err(err).Msg("unexpected proposer") - return nil - } - - if isValidEnvelope && !r.strictMode { - r.logger.Info().Uint64("height", header.Height()).Msg("valid DA envelope detected, switching to STRICT MODE") - r.strictMode = true - } - - // Optimistically mark as DA included - // This has to be done for all fetched DA headers prior to validation because P2P does not confirm - // da inclusion. This is not an issue, as an invalid header will be rejected. There cannot be hash collisions. - headerHash := header.MemoizeHash().String() - r.cache.SetHeaderDAIncluded(headerHash, daHeight, header.Height()) - - r.logger.Debug(). - Str("header_hash", headerHash). - Uint64("da_height", daHeight). - Uint64("height", header.Height()). - Msg("optimistically marked header as DA included") - - return header -} - -// tryDecodeData attempts to decode a blob as signed data -func (r *daRetriever) tryDecodeData(bz []byte, daHeight uint64) *types.Data { - var signedData types.SignedData - if err := signedData.UnmarshalBinary(bz); err != nil { - return nil - } - - // Skip completely empty data - if len(signedData.Txs) == 0 && len(signedData.Signature) == 0 { - return nil - } - - // Validate signature using the configured provider - if err := r.assertValidSignedData(&signedData); err != nil { - r.logger.Debug().Err(err).Msg("invalid signed data") - return nil - } - - // Mark as DA included - dataHash := signedData.Data.DACommitment().String() - r.cache.SetDataDAIncluded(dataHash, daHeight, signedData.Height()) - - r.logger.Debug(). - Str("data_hash", dataHash). - Uint64("da_height", daHeight). - Uint64("height", signedData.Height()). - Msg("data marked as DA included") - - return &signedData.Data -} - // assertExpectedProposer validates the proposer address func (r *daRetriever) assertExpectedProposer(proposerAddr []byte) error { return assertExpectedProposer(r.genesis, proposerAddr) } - -// assertValidSignedData validates signed data using the configured signature provider -func (r *daRetriever) assertValidSignedData(signedData *types.SignedData) error { - return assertValidSignedData(signedData, r.genesis) -} - -// isEmptyDataExpected checks if empty data is expected for a header -func isEmptyDataExpected(header *types.SignedHeader) bool { - return len(header.DataHash) == 0 || bytes.Equal(header.DataHash, common.DataHashForEmptyTxs) -} - -// createEmptyDataForHeader creates empty data for a header -func createEmptyDataForHeader(_ context.Context, header *types.SignedHeader) *types.Data { - return &types.Data{ - Txs: make(types.Txs, 0), - Metadata: &types.Metadata{ - ChainID: header.ChainID(), - Height: header.Height(), - Time: header.BaseHeader.Time, - LastDataHash: nil, // LastDataHash must be filled in the syncer, as it is not available here since block n-1 has not been processed yet. - }, - } -} diff --git a/block/internal/syncing/da_retriever_strict_test.go b/block/internal/syncing/da_retriever_strict_test.go deleted file mode 100644 index 449a7aa4d2..0000000000 --- a/block/internal/syncing/da_retriever_strict_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package syncing - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/evstack/ev-node/pkg/config" - "github.com/evstack/ev-node/pkg/genesis" - "github.com/evstack/ev-node/types" -) - -func TestDARetriever_StrictEnvelopeMode_Switch(t *testing.T) { - // Setup keys - addr, pub, signer := buildSyncTestSigner(t) - gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} - - r := newTestDARetriever(t, nil, config.DefaultConfig(), gen) - - // 1. Create a Legacy Header (SignedHeader marshaled directly) - // This simulates old blobs on the network before the upgrade. - legacyHeader := &types.SignedHeader{ - Header: types.Header{ - BaseHeader: types.BaseHeader{ChainID: gen.ChainID, Height: 1, Time: uint64(time.Now().UnixNano())}, - ProposerAddress: addr, - }, - Signer: types.Signer{PubKey: pub, Address: addr}, - } - // Sign it - bz, err := types.DefaultAggregatorNodeSignatureBytesProvider(&legacyHeader.Header) - require.NoError(t, err) - sig, err := signer.Sign(t.Context(), bz) - require.NoError(t, err) - legacyHeader.Signature = sig - - legacyBlob, err := legacyHeader.MarshalBinary() - require.NoError(t, err) - - // 2. Create an Envelope Header (DAHeaderEnvelope) - // This simulates a new blob after upgrade. - envelopeHeader := &types.SignedHeader{ - Header: types.Header{ - BaseHeader: types.BaseHeader{ChainID: gen.ChainID, Height: 2, Time: uint64(time.Now().UnixNano())}, - ProposerAddress: addr, - }, - Signer: types.Signer{PubKey: pub, Address: addr}, - } - // Sign content - bz2, err := types.DefaultAggregatorNodeSignatureBytesProvider(&envelopeHeader.Header) - require.NoError(t, err) - sig2, err := signer.Sign(t.Context(), bz2) - require.NoError(t, err) - envelopeHeader.Signature = sig2 - - // Create Envelope - // We need to sign the envelope itself. - // The `SubmitHeaders` logic wraps it. We emulate it here using `MarshalDAEnvelope`. - // First get canonical content bytes (fields 1-3) - contentBytes, err := envelopeHeader.MarshalBinary() - require.NoError(t, err) - // Sign envelope - envSig, err := signer.Sign(t.Context(), contentBytes) - require.NoError(t, err) - // Marshal to envelope - envelopeBlob, err := envelopeHeader.MarshalDAEnvelope(envSig) - require.NoError(t, err) - - // --- Test Scenario --- - - // A. Initial State: StrictMode is false. Legacy blob should be accepted. - assert.False(t, r.strictMode) - - decodedLegacy := r.tryDecodeHeader(legacyBlob, 100) - require.NotNil(t, decodedLegacy) - assert.Equal(t, uint64(1), decodedLegacy.Height()) - - // StrictMode should still be false because it was a legacy blob - assert.False(t, r.strictMode) - - // B. Receiving Envelope: Should be accepted and Switch StrictMode to true. - decodedEnvelope := r.tryDecodeHeader(envelopeBlob, 101) - require.NotNil(t, decodedEnvelope) - assert.Equal(t, uint64(2), decodedEnvelope.Height()) - - assert.True(t, r.strictMode, "retriever should have switched to strict mode") - - // C. Receiving Legacy again: Should be REJECTED now. - // We reuse the same legacyBlob (or a new one, doesn't matter, structure is legacy). - decodedLegacyAgain := r.tryDecodeHeader(legacyBlob, 102) - assert.Nil(t, decodedLegacyAgain, "legacy blob should be rejected in strict mode") - - // D. Receiving Envelope again: Should still be accepted. - decodedEnvelopeAgain := r.tryDecodeHeader(envelopeBlob, 103) - require.NotNil(t, decodedEnvelopeAgain) -} diff --git a/block/internal/syncing/da_retriever_test.go b/block/internal/syncing/da_retriever_test.go index 3b587def1f..4ce8ae9e11 100644 --- a/block/internal/syncing/da_retriever_test.go +++ b/block/internal/syncing/da_retriever_test.go @@ -2,14 +2,12 @@ package syncing import ( "context" - "errors" "fmt" "testing" "time" ds "github.com/ipfs/go-datastore" dssync "github.com/ipfs/go-datastore/sync" - "github.com/libp2p/go-libp2p/core/crypto" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -20,7 +18,6 @@ import ( "github.com/evstack/ev-node/pkg/config" datypes "github.com/evstack/ev-node/pkg/da/types" "github.com/evstack/ev-node/pkg/genesis" - signerpkg "github.com/evstack/ev-node/pkg/signer" "github.com/evstack/ev-node/pkg/store" "github.com/evstack/ev-node/test/mocks" "github.com/evstack/ev-node/types" @@ -32,11 +29,7 @@ func newTestDARetriever(t *testing.T, mockClient *mocks.MockClient, cfg config.C if cfg.DA.Namespace == "" { cfg.DA.Namespace = "test-ns" } - if cfg.DA.DataNamespace == "" { - cfg.DA.DataNamespace = "test-data-ns" - } - // Create an in-memory store for the cache memDS := dssync.MutexWrap(ds.NewMapDatastore()) st := store.New(memDS) @@ -48,114 +41,57 @@ func newTestDARetriever(t *testing.T, mockClient *mocks.MockClient, cfg config.C } // default namespace helpers mockClient.On("GetHeaderNamespace").Return([]byte(cfg.DA.Namespace)).Maybe() - mockClient.On("GetDataNamespace").Return([]byte(cfg.DA.DataNamespace)).Maybe() + mockClient.On("GetDataNamespace").Return([]byte(cfg.DA.Namespace)).Maybe() mockClient.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() mockClient.On("HasForcedInclusionNamespace").Return(false).Maybe() return NewDARetriever(mockClient, cm, gen, zerolog.Nop()) } -// makeSignedDataBytes builds SignedData containing the provided Data and returns its binary encoding -func makeSignedDataBytes(t *testing.T, chainID string, height uint64, proposer []byte, pub crypto.PubKey, signer signerpkg.Signer, txs int) ([]byte, *types.SignedData) { - return makeSignedDataBytesWithTime(t, chainID, height, proposer, pub, signer, txs, uint64(time.Now().UnixNano())) -} - -func makeSignedDataBytesWithTime(t *testing.T, chainID string, height uint64, proposer []byte, pub crypto.PubKey, signer signerpkg.Signer, txs int, timestamp uint64) ([]byte, *types.SignedData) { - d := &types.Data{Metadata: &types.Metadata{ChainID: chainID, Height: height, Time: timestamp}} - if txs > 0 { - d.Txs = make(types.Txs, txs) - for i := range txs { - d.Txs[i] = types.Tx([]byte{byte(height), byte(i)}) - } - } - - // For DA SignedData, sign the Data payload bytes (matches DA submission logic) - payload, _ := d.MarshalBinary() - sig, err := signer.Sign(t.Context(), payload) - require.NoError(t, err) - sd := &types.SignedData{Data: *d, Signature: sig, Signer: types.Signer{PubKey: pub, Address: proposer}} - bin, err := sd.MarshalBinary() - require.NoError(t, err) - return bin, sd -} - -func TestDARetriever_RetrieveFromDA_Invalid(t *testing.T) { - client := mocks.NewMockClient(t) - client.On("GetHeaderNamespace").Return([]byte("test-ns")).Maybe() - client.On("GetDataNamespace").Return([]byte("test-data-ns")).Maybe() - client.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() - client.On("HasForcedInclusionNamespace").Return(false).Maybe() - client.On("RetrieveBlobs", mock.Anything, uint64(42), []byte("test-ns")).Return(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess}}).Once() - client.On("RetrieveBlobs", mock.Anything, uint64(42), []byte("test-data-ns")).Return(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusError, Message: "just invalid"}}).Once() - - r := newTestDARetriever(t, client, config.DefaultConfig(), genesis.Genesis{}) - events, err := r.RetrieveFromDA(context.Background(), 42) - assert.Error(t, err) - assert.Len(t, events, 0) -} - func TestDARetriever_RetrieveFromDA_NotFound(t *testing.T) { client := mocks.NewMockClient(t) client.On("GetHeaderNamespace").Return([]byte("test-ns")).Maybe() - client.On("GetDataNamespace").Return([]byte("test-data-ns")).Maybe() + client.On("GetDataNamespace").Return([]byte("test-ns")).Maybe() client.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() client.On("HasForcedInclusionNamespace").Return(false).Maybe() - client.On("RetrieveBlobs", mock.Anything, uint64(42), []byte("test-ns")).Return(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess}}).Once() - client.On("RetrieveBlobs", mock.Anything, uint64(42), []byte("test-data-ns")).Return(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusNotFound, Message: fmt.Sprintf("%s: whatever", datypes.ErrBlobNotFound.Error())}}).Once() + client.On("RetrieveBlobs", mock.Anything, uint64(42), []byte("test-ns")).Return(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusNotFound, Message: fmt.Sprintf("%s: whatever", datypes.ErrBlobNotFound.Error())}}).Once() r := newTestDARetriever(t, client, config.DefaultConfig(), genesis.Genesis{}) events, err := r.RetrieveFromDA(context.Background(), 42) - assert.True(t, errors.Is(err, datypes.ErrBlobNotFound)) - assert.Len(t, events, 0) -} - -func TestDARetriever_RetrieveFromDA_HeightFromFuture(t *testing.T) { - client := mocks.NewMockClient(t) - client.On("GetHeaderNamespace").Return([]byte("test-ns")).Maybe() - client.On("GetDataNamespace").Return([]byte("test-data-ns")).Maybe() - client.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() - client.On("HasForcedInclusionNamespace").Return(false).Maybe() - client.On("RetrieveBlobs", mock.Anything, uint64(1000), []byte("test-ns")).Return(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess}}).Once() - client.On("RetrieveBlobs", mock.Anything, uint64(1000), []byte("test-data-ns")).Return(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusHeightFromFuture, Message: fmt.Sprintf("%s: later", datypes.ErrHeightFromFuture.Error())}}).Once() - - r := newTestDARetriever(t, client, config.DefaultConfig(), genesis.Genesis{}) - events, derr := r.RetrieveFromDA(context.Background(), 1000) - assert.Error(t, derr) - assert.True(t, errors.Is(derr, datypes.ErrHeightFromFuture)) - assert.Nil(t, events) -} - -func TestDARetriever_RetrieveFromDA_TimeoutFast(t *testing.T) { - client := mocks.NewMockClient(t) - client.On("GetHeaderNamespace").Return([]byte("test-ns")).Maybe() - client.On("GetDataNamespace").Return([]byte("test-data-ns")).Maybe() - client.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() - client.On("HasForcedInclusionNamespace").Return(false).Maybe() - client.On("RetrieveBlobs", mock.Anything, uint64(42), []byte("test-ns")).Return(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusError, Message: context.DeadlineExceeded.Error()}}).Once() - client.On("RetrieveBlobs", mock.Anything, uint64(42), []byte("test-data-ns")).Return(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusContextDeadline, Message: context.DeadlineExceeded.Error()}}).Once() - - r := newTestDARetriever(t, client, config.DefaultConfig(), genesis.Genesis{}) - - events, err := r.RetrieveFromDA(context.Background(), 42) - - // Verify error is returned and contains deadline exceeded information - require.Error(t, err) - assert.Contains(t, err.Error(), "DA retrieval failed") - assert.Contains(t, err.Error(), "context deadline exceeded") + assert.Error(t, err) assert.Len(t, events, 0) } -func TestDARetriever_ProcessBlobs_HeaderAndData_Success(t *testing.T) { - +func TestDARetriever_ProcessBlobs_CombinedBlock(t *testing.T) { addr, pub, signer := buildSyncTestSigner(t) gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} r := newTestDARetriever(t, nil, config.DefaultConfig(), gen) - dataBin, data := makeSignedDataBytes(t, gen.ChainID, 2, addr, pub, signer, 2) - hdrBin, _ := makeSignedHeaderBytes(t, gen.ChainID, 2, addr, pub, signer, nil, &data.Data, nil) + d := &types.Data{ + Metadata: &types.Metadata{ChainID: gen.ChainID, Height: 2, Time: uint64(time.Now().UnixNano())}, + Txs: types.Txs{types.Tx([]byte{2, 0}), types.Tx([]byte{2, 1})}, + } + + dataHash := d.DACommitment() + hdr := &types.SignedHeader{ + Header: types.Header{ + BaseHeader: types.BaseHeader{ChainID: gen.ChainID, Height: 2, Time: uint64(time.Now().UnixNano())}, + DataHash: dataHash, + ProposerAddress: addr, + }, + Signer: types.Signer{PubKey: pub, Address: addr}, + } + + hdrBz, err := hdr.MarshalBinary() + require.NoError(t, err) + sig, err := signer.Sign(t.Context(), hdrBz) + require.NoError(t, err) + + blob, err := common.MarshalBlockBlob(hdr, d, sig) + require.NoError(t, err) - events := r.processBlobs(context.Background(), [][]byte{hdrBin, dataBin}, 77) + events := r.processBlobs([][]byte{blob}, 77) require.Len(t, events, 1) assert.Equal(t, uint64(2), events[0].Header.Height()) assert.Equal(t, uint64(2), events[0].Data.Height()) @@ -170,214 +106,34 @@ func TestDARetriever_ProcessBlobs_HeaderAndData_Success(t *testing.T) { assert.Equal(t, uint64(77), dHeight) } -func TestDARetriever_ProcessBlobs_HeaderOnly_EmptyDataExpected(t *testing.T) { - +func TestDARetriever_ProcessBlobs_EmptyData(t *testing.T) { addr, pub, signer := buildSyncTestSigner(t) gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} r := newTestDARetriever(t, nil, config.DefaultConfig(), gen) - // Header with no data hash present should trigger empty data creation (per current logic) - hb, _ := makeSignedHeaderBytes(t, gen.ChainID, 3, addr, pub, signer, nil, nil, nil) + d := &types.Data{ + Metadata: &types.Metadata{ChainID: gen.ChainID, Height: 3, Time: uint64(time.Now().UnixNano())}, + } + hdr := &types.SignedHeader{ + Header: types.Header{ + BaseHeader: types.BaseHeader{ChainID: gen.ChainID, Height: 3, Time: uint64(time.Now().UnixNano())}, + DataHash: common.DataHashForEmptyTxs, + ProposerAddress: addr, + }, + Signer: types.Signer{PubKey: pub, Address: addr}, + } + + hdrBz, err := hdr.MarshalBinary() + require.NoError(t, err) + sig, err := signer.Sign(t.Context(), hdrBz) + require.NoError(t, err) + + blob, err := common.MarshalBlockBlob(hdr, d, sig) + require.NoError(t, err) - events := r.processBlobs(context.Background(), [][]byte{hb}, 88) + events := r.processBlobs([][]byte{blob}, 88) require.Len(t, events, 1) assert.Equal(t, uint64(3), events[0].Header.Height()) assert.NotNil(t, events[0].Data) assert.Equal(t, uint64(88), events[0].DaHeight) - - hHeight, ok := r.cache.GetHeaderDAIncludedByHash(events[0].Header.Hash().String()) - assert.True(t, ok) - assert.Equal(t, uint64(88), hHeight) - - // empty data is not marked as data included (the submitter component does handle the empty data case) - _, ok = r.cache.GetDataDAIncludedByHash(events[0].Data.DACommitment().String()) - assert.False(t, ok) -} - -func TestDARetriever_TryDecodeHeaderAndData_Basic(t *testing.T) { - - addr, pub, signer := buildSyncTestSigner(t) - gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} - r := newTestDARetriever(t, nil, config.DefaultConfig(), gen) - - hb, sh := makeSignedHeaderBytes(t, gen.ChainID, 5, addr, pub, signer, nil, nil, nil) - gotH := r.tryDecodeHeader(hb, 123) - require.NotNil(t, gotH) - assert.Equal(t, sh.Hash().String(), gotH.Hash().String()) - - db, sd := makeSignedDataBytes(t, gen.ChainID, 5, addr, pub, signer, 1) - gotD := r.tryDecodeData(db, 123) - require.NotNil(t, gotD) - assert.Equal(t, sd.Height(), gotD.Height()) - - // invalid data fails - assert.Nil(t, r.tryDecodeHeader([]byte("junk"), 1)) - assert.Nil(t, r.tryDecodeData([]byte("junk"), 1)) -} - -func TestDARetriever_tryDecodeData_InvalidSignatureOrProposer(t *testing.T) { - - goodAddr, pub, signer := buildSyncTestSigner(t) - badAddr := []byte("not-the-proposer") - gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: badAddr} - r := newTestDARetriever(t, nil, config.DefaultConfig(), gen) - - // Signed data is made by goodAddr; retriever expects badAddr -> should be rejected - db, _ := makeSignedDataBytes(t, gen.ChainID, 7, goodAddr, pub, signer, 1) - assert.Nil(t, r.tryDecodeData(db, 55)) -} - -func TestDARetriever_validateBlobResponse(t *testing.T) { - r := &daRetriever{logger: zerolog.Nop()} - // StatusSuccess -> nil - err := r.validateBlobResponse(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess}}, 1) - assert.NoError(t, err) - // StatusError -> error - err = r.validateBlobResponse(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusError, Message: "fail"}}, 1) - assert.Error(t, err) - // StatusHeightFromFuture -> specific error - err = r.validateBlobResponse(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusHeightFromFuture}}, 1) - assert.Error(t, err) - assert.True(t, errors.Is(err, datypes.ErrHeightFromFuture)) -} - -func TestDARetriever_RetrieveFromDA_TwoNamespaces_Success(t *testing.T) { - - addr, pub, signer := buildSyncTestSigner(t) - gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} - - // Prepare header/data blobs - dataBin, data := makeSignedDataBytes(t, gen.ChainID, 9, addr, pub, signer, 1) - hdrBin, _ := makeSignedHeaderBytes(t, gen.ChainID, 9, addr, pub, signer, nil, &data.Data, nil) - - cfg := config.DefaultConfig() - cfg.DA.Namespace = "nsHdr" - cfg.DA.DataNamespace = "nsData" - - client := mocks.NewMockClient(t) - client.On("GetHeaderNamespace").Return([]byte("nsHdr")).Maybe() - client.On("GetDataNamespace").Return([]byte("nsData")).Maybe() - client.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() - client.On("HasForcedInclusionNamespace").Return(false).Maybe() - client.On("RetrieveBlobs", mock.Anything, uint64(1234), []byte("nsHdr")).Return(datypes.ResultRetrieve{ - BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess, IDs: [][]byte{[]byte("h1")}, Timestamp: time.Now()}, - Data: [][]byte{hdrBin}, - }).Once() - client.On("RetrieveBlobs", mock.Anything, uint64(1234), []byte("nsData")).Return(datypes.ResultRetrieve{ - BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess, IDs: [][]byte{[]byte("d1")}, Timestamp: time.Now()}, - Data: [][]byte{dataBin}, - }).Once() - - r := newTestDARetriever(t, client, cfg, gen) - - events, derr := r.RetrieveFromDA(context.Background(), 1234) - require.NoError(t, derr) - require.Len(t, events, 1) - assert.Equal(t, uint64(9), events[0].Header.Height()) - assert.Equal(t, uint64(9), events[0].Data.Height()) -} - -func TestDARetriever_ProcessBlobs_CrossDAHeightMatching(t *testing.T) { - - addr, pub, signer := buildSyncTestSigner(t) - gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} - - r := newTestDARetriever(t, nil, config.DefaultConfig(), gen) - - // Create header and data for the same block height but from different DA heights - dataBin, data := makeSignedDataBytes(t, gen.ChainID, 5, addr, pub, signer, 2) - hdrBin, _ := makeSignedHeaderBytes(t, gen.ChainID, 5, addr, pub, signer, nil, &data.Data, nil) - - // Process header from DA height 100 first - events1 := r.processBlobs(context.Background(), [][]byte{hdrBin}, 100) - require.Len(t, events1, 0, "should not create event yet - data is missing") - - // Verify header is stored in pending headers - require.Contains(t, r.pendingHeaders, uint64(5), "header should be stored as pending") - - // Process data from DA height 102 - events2 := r.processBlobs(context.Background(), [][]byte{dataBin}, 102) - require.Len(t, events2, 1, "should create event when matching data arrives") - - event := events2[0] - assert.Equal(t, uint64(5), event.Header.Height()) - assert.Equal(t, uint64(5), event.Data.Height()) - assert.Equal(t, uint64(102), event.DaHeight, "DaHeight should be the height where data was processed") - - // Verify pending maps are cleared - require.NotContains(t, r.pendingHeaders, uint64(5), "header should be removed from pending") - require.NotContains(t, r.pendingData, uint64(5), "data should be removed from pending") -} - -func TestDARetriever_ProcessBlobs_MultipleHeadersCrossDAHeightMatching(t *testing.T) { - - addr, pub, signer := buildSyncTestSigner(t) - gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} - - r := newTestDARetriever(t, nil, config.DefaultConfig(), gen) - - // Create multiple headers and data for different block heights - data3Bin, data3 := makeSignedDataBytes(t, gen.ChainID, 3, addr, pub, signer, 1) - data4Bin, data4 := makeSignedDataBytes(t, gen.ChainID, 4, addr, pub, signer, 2) - data5Bin, data5 := makeSignedDataBytes(t, gen.ChainID, 5, addr, pub, signer, 1) - - hdr3Bin, _ := makeSignedHeaderBytes(t, gen.ChainID, 3, addr, pub, signer, nil, &data3.Data, nil) - hdr4Bin, _ := makeSignedHeaderBytes(t, gen.ChainID, 4, addr, pub, signer, nil, &data4.Data, nil) - hdr5Bin, _ := makeSignedHeaderBytes(t, gen.ChainID, 5, addr, pub, signer, nil, &data5.Data, nil) - - // Process multiple headers from DA height 200 - should be stored as pending - events1 := r.processBlobs(context.Background(), [][]byte{hdr3Bin, hdr4Bin, hdr5Bin}, 200) - require.Len(t, events1, 0, "should not create events yet - all data is missing") - - // Verify all headers are stored in pending - require.Contains(t, r.pendingHeaders, uint64(3), "header 3 should be pending") - require.Contains(t, r.pendingHeaders, uint64(4), "header 4 should be pending") - require.Contains(t, r.pendingHeaders, uint64(5), "header 5 should be pending") - - // Process some data from DA height 203 - should create partial events - events2 := r.processBlobs(context.Background(), [][]byte{data3Bin, data5Bin}, 203) - require.Len(t, events2, 2, "should create events for heights 3 and 5") - - // Sort events by height for consistent testing - if events2[0].Header.Height() > events2[1].Header.Height() { - events2[0], events2[1] = events2[1], events2[0] - } - - // Verify event for height 3 - assert.Equal(t, uint64(3), events2[0].Header.Height()) - assert.Equal(t, uint64(3), events2[0].Data.Height()) - assert.Equal(t, uint64(203), events2[0].DaHeight) - - // Verify event for height 5 - assert.Equal(t, uint64(5), events2[1].Header.Height()) - assert.Equal(t, uint64(5), events2[1].Data.Height()) - assert.Equal(t, uint64(203), events2[1].DaHeight) - - // Verify header 4 is still pending (no matching data yet) - require.Contains(t, r.pendingHeaders, uint64(4), "header 4 should still be pending") - require.NotContains(t, r.pendingHeaders, uint64(3), "header 3 should be removed from pending") - require.NotContains(t, r.pendingHeaders, uint64(5), "header 5 should be removed from pending") - - // Process remaining data from DA height 205 - events3 := r.processBlobs(context.Background(), [][]byte{data4Bin}, 205) - require.Len(t, events3, 1, "should create event for height 4") - - // Verify final event for height 4 - assert.Equal(t, uint64(4), events3[0].Header.Height()) - assert.Equal(t, uint64(4), events3[0].Data.Height()) - assert.Equal(t, uint64(205), events3[0].DaHeight) - - // Verify all pending maps are now clear - require.NotContains(t, r.pendingHeaders, uint64(4), "header 4 should be removed from pending") - require.Len(t, r.pendingHeaders, 0, "all headers should be processed") - require.Len(t, r.pendingData, 0, "all data should be processed") -} - -func Test_isEmptyDataExpected(t *testing.T) { - h := &types.SignedHeader{} - // when DataHash is nil/empty -> expected empty - assert.True(t, isEmptyDataExpected(h)) - // when equals to predefined emptyTxs hash -> expected empty - h.DataHash = common.DataHashForEmptyTxs - assert.True(t, isEmptyDataExpected(h)) } diff --git a/block/internal/syncing/p2p_handler.go b/block/internal/syncing/p2p_handler.go deleted file mode 100644 index a3778757a1..0000000000 --- a/block/internal/syncing/p2p_handler.go +++ /dev/null @@ -1,135 +0,0 @@ -package syncing - -import ( - "bytes" - "context" - "fmt" - "sync/atomic" - - "github.com/celestiaorg/go-header" - "github.com/rs/zerolog" - - "github.com/evstack/ev-node/block/internal/cache" - "github.com/evstack/ev-node/block/internal/common" - "github.com/evstack/ev-node/pkg/genesis" - "github.com/evstack/ev-node/types" -) - -type p2pHandler interface { - ProcessHeight(ctx context.Context, height uint64, heightInCh chan<- common.DAHeightEvent) error - SetProcessedHeight(height uint64) -} - -// P2PHandler coordinates block retrieval from P2P stores for the syncer. -// It waits for both header and data to be available at a given height, -// validates their consistency, and emits events to the syncer for processing. -// -// The handler maintains a processedHeight to track the highest block that has been -// successfully validated and sent to the syncer, preventing duplicate processing. -type P2PHandler struct { - headerStore header.Store[*types.P2PSignedHeader] - dataStore header.Store[*types.P2PData] - cache cache.CacheManager - genesis genesis.Genesis - logger zerolog.Logger - - processedHeight atomic.Uint64 -} - -// NewP2PHandler creates a new P2P handler. -func NewP2PHandler( - headerStore header.Store[*types.P2PSignedHeader], - dataStore header.Store[*types.P2PData], - cache cache.CacheManager, - genesis genesis.Genesis, - logger zerolog.Logger, -) *P2PHandler { - return &P2PHandler{ - headerStore: headerStore, - dataStore: dataStore, - cache: cache, - genesis: genesis, - logger: logger.With().Str("component", "p2p_handler").Logger(), - } -} - -// SetProcessedHeight updates the highest processed block height. -func (h *P2PHandler) SetProcessedHeight(height uint64) { - for range 1_000 { - current := h.processedHeight.Load() - if height <= current { - return - } - if h.processedHeight.CompareAndSwap(current, height) { - return - } - } -} - -// ProcessHeight retrieves and validates both header and data for the given height from P2P stores. -// It blocks until both are available, validates consistency (proposer address and data hash match), -// then emits the event to heightInCh or stores it as pending. Updates processedHeight on success. -func (h *P2PHandler) ProcessHeight(ctx context.Context, height uint64, heightInCh chan<- common.DAHeightEvent) error { - if height <= h.processedHeight.Load() { - return nil - } - - p2pHeader, err := h.headerStore.GetByHeight(ctx, height) - if err != nil { - if ctx.Err() == nil { - h.logger.Debug().Uint64("height", height).Err(err).Msg("header unavailable in store") - } - return err - } - if err := h.assertExpectedProposer(p2pHeader.ProposerAddress); err != nil { - h.logger.Debug().Uint64("height", height).Err(err).Msg("invalid header from P2P") - return err - } - - p2pData, err := h.dataStore.GetByHeight(ctx, height) - if err != nil { - if ctx.Err() == nil { - h.logger.Debug().Uint64("height", height).Err(err).Msg("data unavailable in store") - } - return err - } - dataCommitment := p2pData.DACommitment() - if !bytes.Equal(p2pHeader.DataHash[:], dataCommitment[:]) { - err := fmt.Errorf("data hash mismatch: header %x, data %x", p2pHeader.DataHash, dataCommitment) - h.logger.Warn().Uint64("height", height).Err(err).Msg("discarding inconsistent block from P2P") - return err - } - - // Memoize hash before the header enters the event pipeline so that downstream - // callers (processHeightEvent, TrySyncNextBlock) get cache hits. - p2pHeader.MemoizeHash() - - // further header validation (signature) is done in validateBlock. - // we need to be sure that the previous block n-1 was executed before validating block n - event := common.DAHeightEvent{ - Header: p2pHeader.SignedHeader, - Data: p2pData.Data, - Source: common.SourceP2P, - DaHeightHints: [2]uint64{p2pHeader.DAHint(), p2pData.DAHint()}, - } - - select { - case heightInCh <- event: - default: - h.cache.SetPendingEvent(event.Header.Height(), &event) - } - - h.SetProcessedHeight(height) - - h.logger.Debug().Uint64("height", height).Msg("processed event from P2P") - return nil -} - -// assertExpectedProposer validates the proposer address. -func (h *P2PHandler) assertExpectedProposer(proposerAddr []byte) error { - if !bytes.Equal(h.genesis.ProposerAddress, proposerAddr) { - return fmt.Errorf("proposer address mismatch: got %x, expected %x", - proposerAddr, h.genesis.ProposerAddress) - } - return nil -} diff --git a/block/internal/syncing/p2p_handler_test.go b/block/internal/syncing/p2p_handler_test.go deleted file mode 100644 index 8bffc31ede..0000000000 --- a/block/internal/syncing/p2p_handler_test.go +++ /dev/null @@ -1,284 +0,0 @@ -package syncing - -import ( - "context" - crand "crypto/rand" - "errors" - "testing" - "time" - - ds "github.com/ipfs/go-datastore" - dssync "github.com/ipfs/go-datastore/sync" - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/rs/zerolog" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/evstack/ev-node/block/internal/cache" - "github.com/evstack/ev-node/block/internal/common" - "github.com/evstack/ev-node/pkg/config" - "github.com/evstack/ev-node/pkg/genesis" - signerpkg "github.com/evstack/ev-node/pkg/signer" - "github.com/evstack/ev-node/pkg/signer/noop" - "github.com/evstack/ev-node/pkg/store" - extmocks "github.com/evstack/ev-node/test/mocks/external" - "github.com/evstack/ev-node/types" -) - -// buildTestSigner returns an address, pubkey and signer suitable for tests. -func buildTestSigner(t *testing.T) ([]byte, crypto.PubKey, signerpkg.Signer) { - t.Helper() - priv, _, err := crypto.GenerateEd25519Key(crand.Reader) - require.NoError(t, err, "failed to generate ed25519 key for test signer") - n, err := noop.NewNoopSigner(priv) - require.NoError(t, err, "failed to create noop signer from private key") - addr, err := n.GetAddress() - require.NoError(t, err, "failed to derive address from signer") - pub, err := n.GetPublic() - require.NoError(t, err, "failed to derive public key from signer") - return addr, pub, n -} - -// p2pMakeSignedHeader creates a minimally valid SignedHeader for P2P tests. -func p2pMakeSignedHeader(t *testing.T, chainID string, height uint64, proposer []byte, pub crypto.PubKey, signer signerpkg.Signer) *types.P2PSignedHeader { - t.Helper() - hdr := &types.SignedHeader{ - Header: types.Header{ - BaseHeader: types.BaseHeader{ChainID: chainID, Height: height, Time: uint64(time.Now().UnixNano())}, - ProposerAddress: proposer, - }, - Signer: types.Signer{PubKey: pub, Address: proposer}, - } - bz, err := types.DefaultAggregatorNodeSignatureBytesProvider(&hdr.Header) - require.NoError(t, err, "failed to get signature bytes for header") - sig, err := signer.Sign(t.Context(), bz) - require.NoError(t, err, "failed to sign header bytes") - hdr.Signature = sig - return &types.P2PSignedHeader{SignedHeader: hdr} -} - -// P2PTestData aggregates dependencies used by P2P handler tests. -type P2PTestData struct { - Handler *P2PHandler - HeaderStore *extmocks.MockStore[*types.P2PSignedHeader] - DataStore *extmocks.MockStore[*types.P2PData] - Cache cache.CacheManager - Genesis genesis.Genesis - ProposerAddr []byte - ProposerPub crypto.PubKey - Signer signerpkg.Signer -} - -// setupP2P constructs a P2PHandler with mocked go-header stores and a real cache. -func setupP2P(t *testing.T) *P2PTestData { - t.Helper() - proposerAddr, proposerPub, signer := buildTestSigner(t) - - gen := genesis.Genesis{ChainID: "p2p-test", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: proposerAddr} - - headerStoreMock := extmocks.NewMockStore[*types.P2PSignedHeader](t) - dataStoreMock := extmocks.NewMockStore[*types.P2PData](t) - - cfg := config.Config{ - RootDir: t.TempDir(), - } - - // Create an in-memory store for the cache - memDS := dssync.MutexWrap(ds.NewMapDatastore()) - st := store.New(memDS) - - cacheManager, err := cache.NewManager(cfg, st, zerolog.Nop()) - require.NoError(t, err, "failed to create cache manager") - - handler := NewP2PHandler(headerStoreMock, dataStoreMock, cacheManager, gen, zerolog.Nop()) - return &P2PTestData{ - Handler: handler, - HeaderStore: headerStoreMock, - DataStore: dataStoreMock, - Cache: cacheManager, - Genesis: gen, - ProposerAddr: proposerAddr, - ProposerPub: proposerPub, - Signer: signer, - } -} - -func collectEvents(t *testing.T, ch <-chan common.DAHeightEvent, timeout time.Duration) []common.DAHeightEvent { - t.Helper() - var events []common.DAHeightEvent - timer := time.NewTimer(timeout) - defer timer.Stop() - for { - select { - case evt := <-ch: - events = append(events, evt) - case <-timer.C: - return events - default: - if len(events) == 0 { - select { - case evt := <-ch: - events = append(events, evt) - default: - return events - } - } else { - return events - } - } - } -} - -func TestP2PHandler_ProcessHeight_EmitsEventWhenHeaderAndDataPresent(t *testing.T) { - p := setupP2P(t) - ctx := context.Background() - - require.Equal(t, string(p.Genesis.ProposerAddress), string(p.ProposerAddr)) - - header := p2pMakeSignedHeader(t, p.Genesis.ChainID, 5, p.ProposerAddr, p.ProposerPub, p.Signer) - data := &types.P2PData{Data: makeData(p.Genesis.ChainID, 5, 1)} - header.DataHash = data.DACommitment() - bz, err := types.DefaultAggregatorNodeSignatureBytesProvider(&header.Header) - require.NoError(t, err) - sig, err := p.Signer.Sign(t.Context(), bz) - require.NoError(t, err) - header.Signature = sig - - p.HeaderStore.EXPECT().GetByHeight(mock.Anything, uint64(5)).Return(header, nil).Once() - p.DataStore.EXPECT().GetByHeight(mock.Anything, uint64(5)).Return(data, nil).Once() - - ch := make(chan common.DAHeightEvent, 1) - err = p.Handler.ProcessHeight(ctx, 5, ch) - require.NoError(t, err) - - events := collectEvents(t, ch, 50*time.Millisecond) - require.Len(t, events, 1) - require.Equal(t, uint64(5), events[0].Header.Height()) - require.NotNil(t, events[0].Data) -} - -func TestP2PHandler_ProcessHeight_SkipsWhenDataMissing(t *testing.T) { - p := setupP2P(t) - ctx := t.Context() - - header := p2pMakeSignedHeader(t, p.Genesis.ChainID, 7, p.ProposerAddr, p.ProposerPub, p.Signer) - data := &types.P2PData{Data: makeData(p.Genesis.ChainID, 7, 1)} - header.DataHash = data.DACommitment() - bz, err := types.DefaultAggregatorNodeSignatureBytesProvider(&header.Header) - require.NoError(t, err) - sig, err := p.Signer.Sign(ctx, bz) - require.NoError(t, err) - header.Signature = sig - - p.HeaderStore.EXPECT().GetByHeight(mock.Anything, uint64(7)).Return(header, nil).Once() - p.DataStore.EXPECT().GetByHeight(mock.Anything, uint64(7)).Return(nil, errors.New("missing")).Once() - - ch := make(chan common.DAHeightEvent, 1) - err = p.Handler.ProcessHeight(ctx, 7, ch) - require.Error(t, err) - - require.Empty(t, collectEvents(t, ch, 50*time.Millisecond)) -} - -func TestP2PHandler_ProcessHeight_SkipsWhenHeaderMissing(t *testing.T) { - p := setupP2P(t) - ctx := context.Background() - - p.HeaderStore.EXPECT().GetByHeight(mock.Anything, uint64(9)).Return(nil, errors.New("missing")).Once() - - ch := make(chan common.DAHeightEvent, 1) - err := p.Handler.ProcessHeight(ctx, 9, ch) - require.Error(t, err) - - require.Empty(t, collectEvents(t, ch, 50*time.Millisecond)) - p.DataStore.AssertNotCalled(t, "GetByHeight", mock.Anything, uint64(9)) -} - -func TestP2PHandler_ProcessHeight_SkipsOnProposerMismatch(t *testing.T) { - p := setupP2P(t) - ctx := context.Background() - var err error - - badAddr, pub, signer := buildTestSigner(t) - require.NotEqual(t, string(p.Genesis.ProposerAddress), string(badAddr)) - - header := p2pMakeSignedHeader(t, p.Genesis.ChainID, 11, badAddr, pub, signer) - header.DataHash = common.DataHashForEmptyTxs - - p.HeaderStore.EXPECT().GetByHeight(mock.Anything, uint64(11)).Return(header, nil).Once() - - ch := make(chan common.DAHeightEvent, 1) - err = p.Handler.ProcessHeight(ctx, 11, ch) - require.Error(t, err) - - require.Empty(t, collectEvents(t, ch, 50*time.Millisecond)) - p.DataStore.AssertNotCalled(t, "GetByHeight", mock.Anything, uint64(11)) -} - -func TestP2PHandler_ProcessedHeightSkipsPreviouslyHandledBlocks(t *testing.T) { - p := setupP2P(t) - ctx := t.Context() - - // Mark up to height 5 as processed. - p.Handler.SetProcessedHeight(5) - - ch := make(chan common.DAHeightEvent, 1) - - // Heights below or equal to 5 should be skipped without touching the stores. - require.NoError(t, p.Handler.ProcessHeight(ctx, 4, ch)) - require.Empty(t, collectEvents(t, ch, 50*time.Millisecond)) - p.HeaderStore.AssertNotCalled(t, "GetByHeight", mock.Anything, uint64(4)) - p.DataStore.AssertNotCalled(t, "GetByHeight", mock.Anything, uint64(4)) - - // Height 6 should be fetched normally. - header := p2pMakeSignedHeader(t, p.Genesis.ChainID, 6, p.ProposerAddr, p.ProposerPub, p.Signer) - data := &types.P2PData{Data: makeData(p.Genesis.ChainID, 6, 1)} - header.DataHash = data.DACommitment() - bz, err := types.DefaultAggregatorNodeSignatureBytesProvider(&header.Header) - require.NoError(t, err) - sig, err := p.Signer.Sign(ctx, bz) - require.NoError(t, err) - header.Signature = sig - - p.HeaderStore.EXPECT().GetByHeight(mock.Anything, uint64(6)).Return(header, nil).Once() - p.DataStore.EXPECT().GetByHeight(mock.Anything, uint64(6)).Return(data, nil).Once() - - require.NoError(t, p.Handler.ProcessHeight(ctx, 6, ch)) - - events := collectEvents(t, ch, 50*time.Millisecond) - require.Len(t, events, 1) - require.Equal(t, uint64(6), events[0].Header.Height()) -} - -func TestP2PHandler_SetProcessedHeightPreventsDuplicates(t *testing.T) { - p := setupP2P(t) - ctx := t.Context() - - header := p2pMakeSignedHeader(t, p.Genesis.ChainID, 8, p.ProposerAddr, p.ProposerPub, p.Signer) - data := &types.P2PData{Data: makeData(p.Genesis.ChainID, 8, 0)} - header.DataHash = data.DACommitment() - bz, err := types.DefaultAggregatorNodeSignatureBytesProvider(&header.Header) - require.NoError(t, err) - sig, err := p.Signer.Sign(ctx, bz) - require.NoError(t, err) - header.Signature = sig - - p.HeaderStore.EXPECT().GetByHeight(mock.Anything, uint64(8)).Return(header, nil).Once() - p.DataStore.EXPECT().GetByHeight(mock.Anything, uint64(8)).Return(data, nil).Once() - - ch := make(chan common.DAHeightEvent, 1) - require.NoError(t, p.Handler.ProcessHeight(ctx, 8, ch)) - - events := collectEvents(t, ch, 50*time.Millisecond) - require.Len(t, events, 1) - - // Mark the height as processed; a subsequent request should skip store access. - p.Handler.SetProcessedHeight(8) - - p.HeaderStore.AssertExpectations(t) - p.DataStore.AssertExpectations(t) - - // No additional expectations set; if the handler queried the stores again the mock would fail. - require.NoError(t, p.Handler.ProcessHeight(ctx, 8, ch)) - require.Empty(t, collectEvents(t, ch, 50*time.Millisecond)) -} diff --git a/block/internal/syncing/syncer.go b/block/internal/syncing/syncer.go index 40e3c9523f..6d842ead44 100644 --- a/block/internal/syncing/syncer.go +++ b/block/internal/syncing/syncer.go @@ -13,7 +13,6 @@ import ( "sync/atomic" "time" - "github.com/celestiaorg/go-header" "github.com/rs/zerolog" "github.com/evstack/ev-node/block/internal/cache" @@ -42,7 +41,7 @@ const ( fullnessThreshold = 0.8 ) -// Syncer handles block synchronization from DA and P2P sources. +// Syncer handles block synchronization from the DA layer and raft sources. type Syncer struct { // Core components store store.Store @@ -65,19 +64,15 @@ type Syncer struct { daClient da.Client daRetrieverHeight *atomic.Uint64 - // P2P stores - headerStore header.Store[*types.P2PSignedHeader] - dataStore header.Store[*types.P2PData] - // Channels for coordination heightInCh chan common.DAHeightEvent errorCh chan<- error // Channel to report critical execution client failures inFlight atomic.Int64 // Handlers - daRetriever DARetriever - fiRetriever da.ForcedInclusionRetriever - p2pHandler p2pHandler + daRetriever DARetriever + fiRetriever da.ForcedInclusionRetriever + raftRetriever *raftRetriever daFollower DAFollower @@ -90,13 +85,11 @@ type Syncer struct { lastCheckedEpochEnd uint64 // highest epochEnd fully verified so far // Lifecycle - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup - hasCriticalError atomic.Bool + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup - // P2P wait coordination - p2pWaitState atomic.Value // stores p2pWaitState + hasCriticalError atomic.Bool // blockSyncer is the interface used for block sync operations. // defaults to self, but can be wrapped with tracing. @@ -108,12 +101,10 @@ func NewSyncer( store store.Store, exec coreexecutor.Executor, daClient da.Client, - cache cache.Manager, + cacheMgr cache.Manager, metrics *common.Metrics, config config.Config, genesis genesis.Genesis, - headerStore header.Store[*types.P2PSignedHeader], - dataStore header.Store[*types.P2PData], logger zerolog.Logger, options common.BlockOptions, errorCh chan<- error, @@ -125,7 +116,7 @@ func NewSyncer( s := &Syncer{ store: store, exec: exec, - cache: cache, + cache: cacheMgr, metrics: metrics, config: config, genesis: genesis, @@ -133,8 +124,6 @@ func NewSyncer( lastState: &atomic.Pointer[types.State]{}, daClient: daClient, daRetrieverHeight: daRetrieverHeight, - headerStore: headerStore, - dataStore: dataStore, heightInCh: make(chan common.DAHeightEvent, 100), errorCh: errorCh, logger: logger.With().Str("component", "syncer").Logger(), @@ -147,8 +136,8 @@ func NewSyncer( s.raftRetriever = newRaftRetriever(raftNode, genesis, logger, s, func(ctx context.Context, state *raft.RaftBlockState) error { s.logger.Debug().Uint64("header_height", state.LastSubmittedDaHeaderHeight).Uint64("data_height", state.LastSubmittedDaDataHeight).Msg("received raft block state") - cache.SetLastSubmittedHeaderHeight(ctx, state.LastSubmittedDaHeaderHeight) - cache.SetLastSubmittedDataHeight(ctx, state.LastSubmittedDaDataHeight) + cacheMgr.SetLastSubmittedHeaderHeight(ctx, state.LastSubmittedDaHeaderHeight) + cacheMgr.SetLastSubmittedDataHeight(ctx, state.LastSubmittedDaDataHeight) return nil }) } @@ -170,7 +159,7 @@ func (s *Syncer) Start(ctx context.Context) (err error) { ctx, cancel := context.WithCancel(ctx) s.ctx, s.cancel = ctx, cancel - defer func() { //nolint: contextcheck // use new context as parent can be cancelled already + defer func() { if err != nil { _ = s.Stop(context.Background()) } @@ -188,14 +177,6 @@ func (s *Syncer) Start(ctx context.Context) (err error) { s.fiRetriever = da.NewForcedInclusionRetriever(s.daClient, s.logger, s.config.DA.BlockTime.Duration, s.config.Instrumentation.IsTracingEnabled(), s.genesis.DAStartHeight, s.genesis.DAEpochForcedInclusion) s.fiRetriever.Start(ctx) - s.p2pHandler = NewP2PHandler(s.headerStore, s.dataStore, s.cache, s.genesis, s.logger) - - currentHeight, initErr := s.store.Height(ctx) - if initErr != nil { - s.logger.Error().Err(initErr).Msg("failed to set initial processed height for p2p handler") - } else { - s.p2pHandler.SetProcessedHeight(currentHeight) - } if s.raftRetriever != nil { if err = s.raftRetriever.Start(ctx); err != nil { @@ -225,7 +206,7 @@ func (s *Syncer) Start(ctx context.Context) (err error) { return fmt.Errorf("failed to start DA follower: %w", err) } - s.startSyncWorkers(ctx) + s.wg.Go(func() { s.pendingWorkerLoop(ctx) }) s.logger.Info().Msg("syncer started") return nil @@ -238,7 +219,6 @@ func (s *Syncer) Stop(ctx context.Context) error { } s.cancel() - s.cancelP2PWait(0) if s.fiRetriever != nil { s.fiRetriever.Stop() @@ -293,7 +273,6 @@ func (s *Syncer) getLastState() types.State { if state == nil { return types.State{} } - return *state } @@ -354,7 +333,6 @@ func (s *Syncer) initializeState() error { if epochSize := s.genesis.DAEpochForcedInclusion; epochSize > 0 && state.DAHeight >= s.genesis.DAStartHeight { firstEpochEnd := s.genesis.DAStartHeight + epochSize - 1 if state.DAHeight >= firstEpochEnd { - // The last completed epoch end that is fully behind state.DAHeight. elapsed := state.DAHeight - firstEpochEnd completedEpochs := elapsed / epochSize s.lastCheckedEpochEnd = firstEpochEnd + completedEpochs*epochSize @@ -363,14 +341,12 @@ func (s *Syncer) initializeState() error { // Set DA height to the maximum of the genesis start height, the state's DA height, and the cached DA height. // The cache's DaHeight() is initialized from store metadata, so it's always correct even after cache clear. - // Only use cache.DaHeight() when P2P is actively syncing (headerStore has higher height than current state). daHeight := s.genesis.DAStartHeight if state.DAHeight > s.genesis.DAStartHeight { daHeight = max(daHeight, state.DAHeight-1) } - if s.headerStore != nil && s.headerStore.Height() > state.LastBlockHeight { - daHeight = max(daHeight, s.cache.DaHeight()) - } + + daHeight = max(daHeight, s.cache.DaHeight()) // dev mode for da start height if startHeight := s.config.DA.StartHeight; startHeight > 0 { @@ -417,13 +393,6 @@ func (s *Syncer) processLoop(ctx context.Context) { } } -func (s *Syncer) startSyncWorkers(ctx context.Context) { - // DA follower is already started in Start(). - s.wg.Add(2) - go s.pendingWorkerLoop(ctx) - go s.p2pWorkerLoop(ctx) -} - // HasReachedDAHead returns true once the DA follower has caught up to the DA head. // Once set, it stays true. func (s *Syncer) HasReachedDAHead() bool { @@ -439,8 +408,6 @@ func (s *Syncer) PendingCount() int { } func (s *Syncer) pendingWorkerLoop(ctx context.Context) { - defer s.wg.Done() - s.logger.Info().Msg("starting pending worker") defer s.logger.Info().Msg("pending worker stopped") @@ -457,60 +424,6 @@ func (s *Syncer) pendingWorkerLoop(ctx context.Context) { } } -func (s *Syncer) p2pWorkerLoop(ctx context.Context) { - defer s.wg.Done() - - logger := s.logger.With().Str("worker", "p2p").Logger() - logger.Info().Msg("starting P2P worker") - defer logger.Info().Msg("P2P worker stopped") - - for { - select { - case <-ctx.Done(): - return - default: - } - - currentHeight, err := s.store.Height(ctx) - if err != nil { - logger.Error().Err(err).Msg("failed to get current height for P2P worker") - if !s.sleepOrDone(ctx, 50*time.Millisecond) { - return - } - continue - } - - targetHeight := currentHeight + 1 - waitCtx, cancel := context.WithCancel(ctx) - s.setP2PWaitState(targetHeight, cancel) - - err = s.p2pHandler.ProcessHeight(waitCtx, targetHeight, s.heightInCh) - s.cancelP2PWait(targetHeight) - - if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - continue - } - - if waitCtx.Err() == nil { - logger.Warn().Err(err).Uint64("height", targetHeight).Msg("P2P handler failed to process height") - } - - if !s.sleepOrDone(ctx, 50*time.Millisecond) { - return - } - continue - } - - if err := s.waitForStoreHeight(ctx, targetHeight); err != nil { - if errors.Is(err, context.Canceled) { - return - } - logger.Error().Err(err).Uint64("height", targetHeight).Msg("failed waiting for height commit") - } - } -} - func (s *Syncer) waitForGenesis() bool { if delay := time.Until(s.genesis.StartTime); delay > 0 { timer := time.NewTimer(delay) @@ -576,86 +489,7 @@ func (s *Syncer) processHeightEvent(ctx context.Context, event *common.DAHeightE return } - // If this is a P2P event with a DA height hint, trigger targeted DA retrieval - // This allows us to fetch the block directly from the specified DA height instead of sequential scanning - if event.Source == common.SourceP2P { - var daHeightHints []uint64 - switch { - case event.DaHeightHints == [2]uint64{0, 0}: - // empty, nothing to do - case event.DaHeightHints[0] == 0: - // check only data - if _, exists := s.cache.GetDataDAIncludedByHeight(height); !exists { - daHeightHints = []uint64{event.DaHeightHints[1]} - } - case event.DaHeightHints[1] == 0: - // check only header - if _, exists := s.cache.GetHeaderDAIncludedByHeight(height); !exists { - daHeightHints = []uint64{event.DaHeightHints[0]} - } - default: - // check both - if _, exists := s.cache.GetHeaderDAIncludedByHeight(height); !exists { - daHeightHints = []uint64{event.DaHeightHints[0]} - } - if _, exists := s.cache.GetDataDAIncludedByHeight(height); !exists { - daHeightHints = append(daHeightHints, event.DaHeightHints[1]) - } - if len(daHeightHints) == 2 && daHeightHints[0] == daHeightHints[1] { - daHeightHints = daHeightHints[0:1] - } - } - if len(daHeightHints) > 0 { - currentDAHeight := s.daRetrieverHeight.Load() - - // Only fetch the latest DA height if any hint is suspiciously far ahead. - const daHintMaxDrift = uint64(200) - needsValidation := false - for _, h := range daHeightHints { - if h > currentDAHeight+daHintMaxDrift { - needsValidation = true - break - } - } - - var latestDAHeight uint64 - if needsValidation { - var err error - xCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) - latestDAHeight, err = s.daClient.GetLatestDAHeight(xCtx) - cancel() - if err != nil { - s.logger.Warn().Err(err).Msg("failed to fetch latest DA height") - needsValidation = false // ignore error as height is checked in the daRetriever - } - } - - for _, daHeightHint := range daHeightHints { - // Skip if we've already fetched past this height - if daHeightHint < currentDAHeight { - continue - } - - if needsValidation && daHeightHint > latestDAHeight { - s.logger.Warn().Uint64("da_height_hint", daHeightHint). - Uint64("latest_da_height", latestDAHeight). - Msg("ignoring unreasonable DA height hint from P2P") - continue - } - - s.logger.Debug(). - Uint64("height", height). - Uint64("da_height_hint", daHeightHint). - Msg("P2P event with DA height hint, queuing priority DA retrieval") - - // Queue priority DA retrieval - will be processed in fetchDAUntilCaughtUp - s.daFollower.QueuePriorityHeight(daHeightHint) - } - } - } - // Last data must be got from store if the event comes from DA and the data hash is empty. - // When if the event comes from P2P, the sequencer and then all the full nodes contains the data. if event.Source == common.SourceDA && bytes.Equal(event.Header.DataHash, common.DataHashForEmptyTxs) && currentHeight > 0 { _, lastData, err := s.store.GetBlockData(ctx, currentHeight) if err != nil { @@ -665,9 +499,6 @@ func (s *Syncer) processHeightEvent(ctx context.Context, event *common.DAHeightE event.Data.LastDataHash = lastData.Hash() } - // Cancel any P2P wait that might still be blocked on this height, as we have a block for it. - s.cancelP2PWait(height) - // Try to sync the next block if err := s.blockSyncer.TrySyncNextBlock(ctx, event); err != nil { s.logger.Error().Err(err). @@ -737,9 +568,6 @@ func (s *Syncer) trySyncNextBlockWithState(ctx context.Context, event *common.DA // Verify forced inclusion transactions if configured. // The check is actually only performed on DA-sourced blocks. - // P2P nodes aren't actually able to verify forced inclusion txs as DA inclusion happens later - // (so DA hints are not available) and DA hints cannot be trusted. This is a known limitation - // described in the ADR. if event.Source == common.SourceDA { if err := s.VerifyForcedInclusionTxs(ctx, event.DaHeight, data); err != nil { s.logger.Error().Err(err).Uint64("height", nextHeight).Msg("forced inclusion verification failed") @@ -747,7 +575,6 @@ func (s *Syncer) trySyncNextBlockWithState(ctx context.Context, event *common.DA // remove header as da included from cache s.cache.RemoveHeaderDAIncluded(headerHash) s.cache.RemoveDataDAIncluded(data.DACommitment().String()) - return err } } @@ -799,10 +626,6 @@ func (s *Syncer) trySyncNextBlockWithState(ctx context.Context, event *common.DA s.cache.SetDataSeen(data.DACommitment().String(), newState.LastBlockHeight) } - if s.p2pHandler != nil { - s.p2pHandler.SetProcessedHeight(newState.LastBlockHeight) - } - return nil } @@ -869,7 +692,7 @@ func (s *Syncer) ValidateBlock(_ context.Context, currState types.State, data *t // Set custom verifier for aggregator node signature header.SetCustomVerifierForSyncNode(s.options.SyncNodeSignatureBytesProvider) - if err := header.ValidateBasicWithData(data); err != nil { //nolint:contextcheck // validation API does not accept context + if err := header.ValidateBasicWithData(data); err != nil { return fmt.Errorf("invalid header: %w", err) } @@ -1105,25 +928,6 @@ func (s *Syncer) processPendingEvents(ctx context.Context) { } } -func (s *Syncer) waitForStoreHeight(ctx context.Context, target uint64) error { - for { - currentHeight, err := s.store.Height(ctx) - if err != nil { - return err - } - - if currentHeight >= target { - return nil - } - - if !s.sleepOrDone(ctx, 10*time.Millisecond) { - if ctx.Err() != nil { - return ctx.Err() - } - } - } -} - func (s *Syncer) sleepOrDone(ctx context.Context, duration time.Duration) bool { timer := time.NewTimer(duration) defer timer.Stop() @@ -1136,31 +940,6 @@ func (s *Syncer) sleepOrDone(ctx context.Context, duration time.Duration) bool { } } -type p2pWaitState struct { - height uint64 - cancel context.CancelFunc -} - -func (s *Syncer) setP2PWaitState(height uint64, cancel context.CancelFunc) { - s.p2pWaitState.Store(p2pWaitState{height: height, cancel: cancel}) -} - -func (s *Syncer) cancelP2PWait(height uint64) { - val := s.p2pWaitState.Load() - if val == nil { - return - } - state, ok := val.(p2pWaitState) - if !ok || state.cancel == nil { - return - } - - if height == 0 || state.height <= height { - s.p2pWaitState.Store(p2pWaitState{}) - state.cancel() - } -} - // IsSyncedWithRaft checks if the local state is synced with the given raft state, including hash check. func (s *Syncer) IsSyncedWithRaft(raftState *raft.RaftBlockState) (int, error) { state, err := s.store.GetState(s.ctx) diff --git a/block/internal/syncing/syncer_benchmark_test.go b/block/internal/syncing/syncer_benchmark_test.go index b066f2ec71..7256a9e2eb 100644 --- a/block/internal/syncing/syncer_benchmark_test.go +++ b/block/internal/syncing/syncer_benchmark_test.go @@ -20,7 +20,6 @@ import ( "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/pkg/store" testmocks "github.com/evstack/ev-node/test/mocks" - extmocks "github.com/evstack/ev-node/test/mocks/external" "github.com/evstack/ev-node/types" ) @@ -32,18 +31,15 @@ func BenchmarkSyncerIO(b *testing.B) { shuffledTx bool daDelay time.Duration execDelay time.Duration - p2pEnabled bool - p2pDelay time.Duration }{ - "slow producer": {heights: 100, daDelay: 200 * time.Microsecond, execDelay: 0, p2pDelay: 0, p2pEnabled: false}, - "slow consumer": {heights: 100, daDelay: 0, execDelay: 200 * time.Microsecond, p2pDelay: 0, p2pEnabled: false}, + "slow producer": {heights: 100, daDelay: 200 * time.Microsecond, execDelay: 0}, + "slow consumer": {heights: 100, daDelay: 0, execDelay: 200 * time.Microsecond}, } for name, spec := range cases { b.Run(name, func(b *testing.B) { for b.Loop() { - fixt := newBenchFixture(b, spec.heights, spec.shuffledTx, spec.daDelay, spec.execDelay, true) + fixt := newBenchFixture(b, spec.heights, spec.shuffledTx, spec.daDelay, spec.execDelay) - // run both loops ctx := b.Context() go fixt.s.processLoop(ctx) @@ -61,7 +57,7 @@ func BenchmarkSyncerIO(b *testing.B) { follower.Start(ctx) eventCh <- datypes.SubscriptionEvent{Height: spec.heights + daHeightOffset} - fixt.s.startSyncWorkers(ctx) + fixt.s.wg.Go(func() { fixt.s.pendingWorkerLoop(ctx) }) require.Eventually(b, func() bool { processedHeight, _ := fixt.s.store.Height(ctx) @@ -93,7 +89,7 @@ type benchFixture struct { cancel context.CancelFunc } -func newBenchFixture(b *testing.B, totalHeights uint64, shuffledTx bool, daDelay, execDelay time.Duration, includeP2P bool) *benchFixture { +func newBenchFixture(b *testing.B, totalHeights uint64, shuffledTx bool, daDelay, execDelay time.Duration) *benchFixture { b.Helper() ctx, cancel := context.WithCancel(b.Context()) @@ -105,7 +101,6 @@ func newBenchFixture(b *testing.B, totalHeights uint64, shuffledTx bool, daDelay addr, pub, signer := buildSyncTestSigner(b) cfg := config.DefaultConfig() - // keep P2P ticker dormant unless we manually inject P2P events cfg.Node.BlockTime = config.DurationWrapper{Duration: 1} gen := genesis.Genesis{ChainID: "bchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr, DAStartHeight: daHeightOffset} @@ -123,13 +118,11 @@ func newBenchFixture(b *testing.B, totalHeights uint64, shuffledTx bool, daDelay s := NewSyncer( st, mockExec, - nil, // DA injected via mock retriever below + nil, cm, common.NopMetrics(), cfg, gen, - nil, // headerStore not used; we inject P2P directly to channel when needed - nil, // dataStore not used zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), @@ -171,12 +164,5 @@ func newBenchFixture(b *testing.B, totalHeights uint64, shuffledTx bool, daDelay // Attach mocks s.daRetriever = daR - mockP2P := newMockp2pHandler(b) // not used directly in this benchmark path - mockP2P.On("SetProcessedHeight", mock.Anything).Return().Maybe() - s.p2pHandler = mockP2P - headerP2PStore := extmocks.NewMockStore[*types.P2PSignedHeader](b) - s.headerStore = headerP2PStore - dataP2PStore := extmocks.NewMockStore[*types.P2PData](b) - s.dataStore = dataP2PStore return &benchFixture{s: s, st: st, cm: cm, cancel: cancel} } diff --git a/block/internal/syncing/syncer_forced_inclusion_test.go b/block/internal/syncing/syncer_forced_inclusion_test.go index 3c15fde125..5ba4b1f8fc 100644 --- a/block/internal/syncing/syncer_forced_inclusion_test.go +++ b/block/internal/syncing/syncer_forced_inclusion_test.go @@ -20,7 +20,6 @@ import ( "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/pkg/store" testmocks "github.com/evstack/ev-node/test/mocks" - extmocks "github.com/evstack/ev-node/test/mocks/external" "github.com/evstack/ev-node/types" ) @@ -81,13 +80,8 @@ func newForcedInclusionSyncer(t *testing.T, daStart, epochSize uint64) (*Syncer, fiRetriever := da.NewForcedInclusionRetriever(client, zerolog.Nop(), cfg.DA.BlockTime.Duration, false, gen.DAStartHeight, gen.DAEpochForcedInclusion) t.Cleanup(fiRetriever.Stop) - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() s := NewSyncer( st, mockExec, client, cm, common.NopMetrics(), cfg, gen, - mockHeaderStore, mockDataStore, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), nil, ) s.daRetriever = daRetriever @@ -159,13 +153,8 @@ func TestVerifyForcedInclusionTxs_NamespaceNotConfigured(t *testing.T) { fiRetriever := da.NewForcedInclusionRetriever(client, zerolog.Nop(), cfg.DA.BlockTime.Duration, false, gen.DAStartHeight, gen.DAEpochForcedInclusion) t.Cleanup(fiRetriever.Stop) - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() s := NewSyncer( st, mockExec, client, cm, common.NopMetrics(), cfg, gen, - mockHeaderStore, mockDataStore, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), nil, ) if s.fiRetriever != nil { diff --git a/block/internal/syncing/syncer_mock.go b/block/internal/syncing/syncer_mock.go deleted file mode 100644 index aae45399e4..0000000000 --- a/block/internal/syncing/syncer_mock.go +++ /dev/null @@ -1,142 +0,0 @@ -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package syncing - -import ( - "context" - - "github.com/evstack/ev-node/block/internal/common" - mock "github.com/stretchr/testify/mock" -) - -// newMockp2pHandler creates a new instance of mockp2pHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockp2pHandler(t interface { - mock.TestingT - Cleanup(func()) -}) *mockp2pHandler { - mock := &mockp2pHandler{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// mockp2pHandler is an autogenerated mock type for the p2pHandler type -type mockp2pHandler struct { - mock.Mock -} - -type mockp2pHandler_Expecter struct { - mock *mock.Mock -} - -func (_m *mockp2pHandler) EXPECT() *mockp2pHandler_Expecter { - return &mockp2pHandler_Expecter{mock: &_m.Mock} -} - -// ProcessHeight provides a mock function for the type mockp2pHandler -func (_mock *mockp2pHandler) ProcessHeight(ctx context.Context, height uint64, heightInCh chan<- common.DAHeightEvent) error { - ret := _mock.Called(ctx, height, heightInCh) - - if len(ret) == 0 { - panic("no return value specified for ProcessHeight") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, uint64, chan<- common.DAHeightEvent) error); ok { - r0 = returnFunc(ctx, height, heightInCh) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// mockp2pHandler_ProcessHeight_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ProcessHeight' -type mockp2pHandler_ProcessHeight_Call struct { - *mock.Call -} - -// ProcessHeight is a helper method to define mock.On call -// - ctx context.Context -// - height uint64 -// - heightInCh chan<- common.DAHeightEvent -func (_e *mockp2pHandler_Expecter) ProcessHeight(ctx interface{}, height interface{}, heightInCh interface{}) *mockp2pHandler_ProcessHeight_Call { - return &mockp2pHandler_ProcessHeight_Call{Call: _e.mock.On("ProcessHeight", ctx, height, heightInCh)} -} - -func (_c *mockp2pHandler_ProcessHeight_Call) Run(run func(ctx context.Context, height uint64, heightInCh chan<- common.DAHeightEvent)) *mockp2pHandler_ProcessHeight_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 uint64 - if args[1] != nil { - arg1 = args[1].(uint64) - } - var arg2 chan<- common.DAHeightEvent - if args[2] != nil { - arg2 = args[2].(chan<- common.DAHeightEvent) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *mockp2pHandler_ProcessHeight_Call) Return(err error) *mockp2pHandler_ProcessHeight_Call { - _c.Call.Return(err) - return _c -} - -func (_c *mockp2pHandler_ProcessHeight_Call) RunAndReturn(run func(ctx context.Context, height uint64, heightInCh chan<- common.DAHeightEvent) error) *mockp2pHandler_ProcessHeight_Call { - _c.Call.Return(run) - return _c -} - -// SetProcessedHeight provides a mock function for the type mockp2pHandler -func (_mock *mockp2pHandler) SetProcessedHeight(height uint64) { - _mock.Called(height) - return -} - -// mockp2pHandler_SetProcessedHeight_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetProcessedHeight' -type mockp2pHandler_SetProcessedHeight_Call struct { - *mock.Call -} - -// SetProcessedHeight is a helper method to define mock.On call -// - height uint64 -func (_e *mockp2pHandler_Expecter) SetProcessedHeight(height interface{}) *mockp2pHandler_SetProcessedHeight_Call { - return &mockp2pHandler_SetProcessedHeight_Call{Call: _e.mock.On("SetProcessedHeight", height)} -} - -func (_c *mockp2pHandler_SetProcessedHeight_Call) Run(run func(height uint64)) *mockp2pHandler_SetProcessedHeight_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 uint64 - if args[0] != nil { - arg0 = args[0].(uint64) - } - run( - arg0, - ) - }) - return _c -} - -func (_c *mockp2pHandler_SetProcessedHeight_Call) Return() *mockp2pHandler_SetProcessedHeight_Call { - _c.Call.Return() - return _c -} - -func (_c *mockp2pHandler_SetProcessedHeight_Call) RunAndReturn(run func(height uint64)) *mockp2pHandler_SetProcessedHeight_Call { - _c.Run(run) - return _c -} diff --git a/block/internal/syncing/syncer_test.go b/block/internal/syncing/syncer_test.go index 67c87e06ed..42172a027b 100644 --- a/block/internal/syncing/syncer_test.go +++ b/block/internal/syncing/syncer_test.go @@ -5,7 +5,6 @@ import ( crand "crypto/rand" "crypto/sha512" "errors" - "math" "sync" "sync/atomic" "testing" @@ -22,7 +21,6 @@ import ( "github.com/evstack/ev-node/block/internal/cache" "github.com/evstack/ev-node/block/internal/common" - "github.com/evstack/ev-node/block/internal/da" "github.com/evstack/ev-node/core/execution" "github.com/evstack/ev-node/pkg/config" datypes "github.com/evstack/ev-node/pkg/da/types" @@ -32,7 +30,6 @@ import ( "github.com/evstack/ev-node/pkg/signer/noop" "github.com/evstack/ev-node/pkg/store" testmocks "github.com/evstack/ev-node/test/mocks" - extmocks "github.com/evstack/ev-node/test/mocks/external" "github.com/evstack/ev-node/types" ) @@ -113,7 +110,7 @@ func makeSignedHeaderBytes( return bin, hdr } -func setupMockDAClient(tb testing.TB) (da.Client, chan datypes.SubscriptionEvent) { +func setupMockDAClient(tb testing.TB) (*testmocks.MockClient, chan datypes.SubscriptionEvent) { mockClient := testmocks.NewMockClient(tb) eventCh := make(chan datypes.SubscriptionEvent, 1) mockClient.EXPECT().Subscribe(mock.Anything, mock.Anything, mock.Anything).Return((<-chan datypes.SubscriptionEvent)(eventCh), nil).Maybe() @@ -136,6 +133,26 @@ func makeData(chainID string, height uint64, txs int) *types.Data { return d } +// makeSignedDataBytes builds SignedData containing the provided Data and returns its binary encoding +func makeSignedDataBytes(t *testing.T, chainID string, height uint64, proposer []byte, pub crypto.PubKey, signer signerpkg.Signer, txs int) ([]byte, *types.SignedData) { + t.Helper() + d := &types.Data{Metadata: &types.Metadata{ChainID: chainID, Height: height, Time: uint64(time.Now().UnixNano())}} + if txs > 0 { + d.Txs = make(types.Txs, txs) + for i := range txs { + d.Txs[i] = types.Tx([]byte{byte(height), byte(i)}) + } + } + + payload, _ := d.MarshalBinary() + sig, err := signer.Sign(t.Context(), payload) + require.NoError(t, err) + sd := &types.SignedData{Data: *d, Signature: sig, Signer: types.Signer{PubKey: pub, Address: proposer}} + bin, err := sd.MarshalBinary() + require.NoError(t, err) + return bin, sd +} + func TestSyncer_validateBlock_DataHashMismatch(t *testing.T) { ds := dssync.MutexWrap(datastore.NewMapDatastore()) st := store.New(ds) @@ -150,11 +167,6 @@ func TestSyncer_validateBlock_DataHashMismatch(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), nil).Once() - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() - s := NewSyncer( st, mockExec, @@ -163,8 +175,6 @@ func TestSyncer_validateBlock_DataHashMismatch(t *testing.T) { common.NopMetrics(), cfg, gen, - mockHeaderStore, - mockDataStore, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), @@ -206,11 +216,6 @@ func TestProcessHeightEvent_SyncsAndUpdatesState(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), nil).Once() - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() - errChan := make(chan error, 1) s := NewSyncer( st, @@ -220,8 +225,6 @@ func TestProcessHeightEvent_SyncsAndUpdatesState(t *testing.T) { common.NopMetrics(), cfg, gen, - mockHeaderStore, - mockDataStore, zerolog.Nop(), common.DefaultBlockOptions(), errChan, @@ -266,11 +269,6 @@ func TestSequentialBlockSync(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), nil).Once() - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() - errChan := make(chan error, 1) s := NewSyncer( st, @@ -280,8 +278,6 @@ func TestSequentialBlockSync(t *testing.T) { common.NopMetrics(), cfg, gen, - mockHeaderStore, - mockDataStore, zerolog.Nop(), common.DefaultBlockOptions(), errChan, @@ -348,8 +344,6 @@ func TestSyncer_RecoverFromRaft_BootstrapsStateWhenUninitialized(t *testing.T) { } mockExec := testmocks.NewMockExecutor(t) - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) s := NewSyncer( st, mockExec, @@ -358,8 +352,6 @@ func TestSyncer_RecoverFromRaft_BootstrapsStateWhenUninitialized(t *testing.T) { common.NopMetrics(), config.DefaultConfig(), gen, - mockHeaderStore, - mockDataStore, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), @@ -407,8 +399,6 @@ func TestSyncer_RecoverFromRaft_KeepsStrictValidationAfterStateExists(t *testing } mockExec := testmocks.NewMockExecutor(t) - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) s := NewSyncer( st, mockExec, @@ -417,8 +407,6 @@ func TestSyncer_RecoverFromRaft_KeepsStrictValidationAfterStateExists(t *testing common.NopMetrics(), config.DefaultConfig(), gen, - mockHeaderStore, - mockDataStore, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), @@ -467,8 +455,6 @@ func TestSyncer_RecoverFromRaft_LocalAheadOfStaleSnapshot(t *testing.T) { } mockExec := testmocks.NewMockExecutor(t) - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) s := NewSyncer( st, mockExec, @@ -477,8 +463,6 @@ func TestSyncer_RecoverFromRaft_LocalAheadOfStaleSnapshot(t *testing.T) { common.NopMetrics(), config.DefaultConfig(), gen, - mockHeaderStore, - mockDataStore, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), @@ -503,9 +487,6 @@ func TestSyncer_RecoverFromRaft_LocalAheadOfStaleSnapshot(t *testing.T) { })) require.NoError(t, batch.Commit()) - // Simulate EVM at height 1, raft snapshot stale at height 0 — but there is no - // block 0 to check, so use height 1 EVM vs stale snapshot at height 0. - // More realistic: EVM at height 2, raft snapshot at height 1. // Build a second block and advance the store state to height 2. data2 := makeData(gen.ChainID, 2, 0) headerBz2, hdr2 := makeSignedHeaderBytes(t, gen.ChainID, 2, addr, pub, signer, []byte("app2"), data2, hdr1.Hash()) @@ -588,8 +569,6 @@ func TestSyncer_Stop_CallsRaftRetrieverStop(t *testing.T) { common.NopMetrics(), config.DefaultConfig(), genesis.Genesis{}, - nil, - nil, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), @@ -672,18 +651,6 @@ func TestSyncLoopPersistState(t *testing.T) { dummyExec := execution.NewDummyExecutor() - // Create mock stores for P2P - mockHeaderStore := extmocks.NewMockStore[*types.SignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - - mockDataStore := extmocks.NewMockStore[*types.Data](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() - - mockP2PHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockP2PHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockP2PDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockP2PDataStore.EXPECT().Height().Return(uint64(0)).Maybe() - errorCh := make(chan error, 1) syncerInst1 := NewSyncer( st, @@ -693,8 +660,6 @@ func TestSyncLoopPersistState(t *testing.T) { common.NopMetrics(), cfg, gen, - mockP2PHeaderStore, - mockP2PDataStore, zerolog.Nop(), common.DefaultBlockOptions(), errorCh, @@ -704,11 +669,9 @@ func TestSyncLoopPersistState(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) syncerInst1.ctx = ctx - daRtrMock, p2pHndlMock := NewMockDARetriever(t), newMockp2pHandler(t) - p2pHndlMock.On("ProcessHeight", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() - p2pHndlMock.On("SetProcessedHeight", mock.Anything).Return().Maybe() + daRtrMock := NewMockDARetriever(t) - syncerInst1.daRetriever, syncerInst1.p2pHandler = daRtrMock, p2pHndlMock + syncerInst1.daRetriever = daRtrMock syncerInst1.daFollower = NewDAFollower(DAFollowerConfig{ Retriever: daRtrMock, Logger: zerolog.Nop(), @@ -770,7 +733,7 @@ func TestSyncLoopPersistState(t *testing.T) { }).(*daFollower) require.NoError(t, follower1.Start(ctx)) eventCh <- datypes.SubscriptionEvent{Height: myFutureDAHeight} - syncerInst1.startSyncWorkers(ctx) + syncerInst1.wg.Go(func() { syncerInst1.pendingWorkerLoop(ctx) }) syncerInst1.wg.Wait() requireEmptyChan(t, errorCh) @@ -783,7 +746,6 @@ func TestSyncLoopPersistState(t *testing.T) { // then daRtrMock.AssertExpectations(t) - p2pHndlMock.AssertExpectations(t) require.Len(t, syncerInst1.heightInCh, 0) // and all processed - verify no events remain at heights we tested @@ -805,8 +767,6 @@ func TestSyncLoopPersistState(t *testing.T) { common.NopMetrics(), cfg, gen, - mockP2PHeaderStore, - mockP2PDataStore, zerolog.Nop(), common.DefaultBlockOptions(), make(chan error, 1), @@ -817,11 +777,9 @@ func TestSyncLoopPersistState(t *testing.T) { ctx, cancel = context.WithCancel(t.Context()) t.Cleanup(cancel) syncerInst2.ctx = ctx - daRtrMock, p2pHndlMock = NewMockDARetriever(t), newMockp2pHandler(t) - p2pHndlMock.On("ProcessHeight", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() - p2pHndlMock.On("SetProcessedHeight", mock.Anything).Return().Maybe() + daRtrMock = NewMockDARetriever(t) - syncerInst2.daRetriever, syncerInst2.p2pHandler = daRtrMock, p2pHndlMock + syncerInst2.daRetriever = daRtrMock syncerInst2.daFollower = NewDAFollower(DAFollowerConfig{ Retriever: daRtrMock, Logger: zerolog.Nop(), @@ -854,7 +812,7 @@ func TestSyncLoopPersistState(t *testing.T) { }).(*daFollower) follower2.Start(ctx) eventCh2 <- datypes.SubscriptionEvent{Height: syncerInst2.daRetrieverHeight.Load() + 1} - syncerInst2.startSyncWorkers(ctx) + syncerInst2.wg.Go(func() { syncerInst2.pendingWorkerLoop(ctx) }) syncerInst2.wg.Wait() t.Log("sync workers exited") @@ -1029,407 +987,6 @@ func requireEmptyChan(t *testing.T, errorCh chan error) { } } -func TestProcessHeightEvent_TriggersAsyncDARetrieval(t *testing.T) { - ds := dssync.MutexWrap(datastore.NewMapDatastore()) - st := store.New(ds) - cm, err := cache.NewManager(config.DefaultConfig(), st, zerolog.Nop()) - require.NoError(t, err) - - addr, _, _ := buildSyncTestSigner(t) - cfg := config.DefaultConfig() - gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} - - mockExec := testmocks.NewMockExecutor(t) - mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), nil).Once() - - // Use a mock DA client that reports a latest height above the hint - mockDAClient := testmocks.NewMockClient(t) - mockDAClient.EXPECT().GetLatestDAHeight(mock.Anything).Return(uint64(200), nil).Maybe() - - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() - - s := NewSyncer( - st, - mockExec, - mockDAClient, - cm, - common.NopMetrics(), - cfg, - gen, - mockHeaderStore, - mockDataStore, - zerolog.Nop(), - common.DefaultBlockOptions(), - make(chan error, 1), - nil, - ) - require.NoError(t, s.initializeState()) - s.ctx = context.Background() - - // Create a real daRetriever to test priority queue - s.daRetriever = NewDARetriever(nil, cm, gen, zerolog.Nop()) - s.daFollower = NewDAFollower(DAFollowerConfig{ - Retriever: s.daRetriever, - Logger: zerolog.Nop(), - EventSink: common.EventSinkFunc(func(_ context.Context, _ common.DAHeightEvent) error { return nil }), - Namespace: []byte("ns"), - StartDAHeight: 0, - }) - - // Create event with DA height hint - evt := common.DAHeightEvent{ - Header: &types.SignedHeader{Header: types.Header{BaseHeader: types.BaseHeader{ChainID: "c", Height: 2}}}, - Data: &types.Data{Metadata: &types.Metadata{ChainID: "c", Height: 2}}, - Source: common.SourceP2P, - DaHeightHints: [2]uint64{100, 100}, - } - - // Set the store height to 1 so the event can be processed as "next". - batch, err := st.NewBatch(context.Background()) - require.NoError(t, err) - require.NoError(t, batch.SetHeight(1)) - require.NoError(t, batch.Commit()) - - s.processHeightEvent(t.Context(), &evt) - - // Verify that the priority height was queued in the daRetriever - priorityHeight := s.daFollower.(*daFollower).popPriorityHeight() - assert.Equal(t, uint64(100), priorityHeight) -} - -func TestProcessHeightEvent_RejectsUnreasonableDAHint(t *testing.T) { - ds := dssync.MutexWrap(datastore.NewMapDatastore()) - st := store.New(ds) - cm, err := cache.NewManager(config.DefaultConfig(), st, zerolog.Nop()) - require.NoError(t, err) - - addr, _, _ := buildSyncTestSigner(t) - cfg := config.DefaultConfig() - gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} - - mockExec := testmocks.NewMockExecutor(t) - mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), nil).Once() - - // Mock DA client reports latest DA height of 100 - mockDAClient := testmocks.NewMockClient(t) - mockDAClient.EXPECT().GetLatestDAHeight(mock.Anything).Return(uint64(100), nil).Maybe() - - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() - - s := NewSyncer( - st, - mockExec, - mockDAClient, - cm, - common.NopMetrics(), - cfg, - gen, - mockHeaderStore, - mockDataStore, - zerolog.Nop(), - common.DefaultBlockOptions(), - make(chan error, 1), - nil, - ) - require.NoError(t, s.initializeState()) - s.ctx = context.Background() - s.daRetriever = NewDARetriever(nil, cm, gen, zerolog.Nop()) - s.daFollower = NewDAFollower(DAFollowerConfig{ - Retriever: s.daRetriever, - Logger: zerolog.Nop(), - EventSink: common.EventSinkFunc(func(_ context.Context, _ common.DAHeightEvent) error { return nil }), - Namespace: []byte("ns"), - StartDAHeight: 0, - }) - - // Set store height to 1 so event at height 2 is "next" - batch, err := st.NewBatch(context.Background()) - require.NoError(t, err) - require.NoError(t, batch.SetHeight(1)) - require.NoError(t, batch.Commit()) - - // Send a malicious P2P hint with math.MaxUint64 - evt := common.DAHeightEvent{ - Header: &types.SignedHeader{Header: types.Header{BaseHeader: types.BaseHeader{ChainID: "c", Height: 2}}}, - Data: &types.Data{Metadata: &types.Metadata{ChainID: "c", Height: 2}}, - Source: common.SourceP2P, - DaHeightHints: [2]uint64{math.MaxUint64, math.MaxUint64}, - } - - s.processHeightEvent(t.Context(), &evt) - - // Verify that NO priority height was queued — the hint was rejected - priorityHeight := s.daFollower.(*daFollower).popPriorityHeight() - assert.Equal(t, uint64(0), priorityHeight, "unreasonable DA hint should be rejected") -} - -func TestProcessHeightEvent_AcceptsValidDAHint(t *testing.T) { - ds := dssync.MutexWrap(datastore.NewMapDatastore()) - st := store.New(ds) - cm, err := cache.NewManager(config.DefaultConfig(), st, zerolog.Nop()) - require.NoError(t, err) - - addr, _, _ := buildSyncTestSigner(t) - cfg := config.DefaultConfig() - gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} - - mockExec := testmocks.NewMockExecutor(t) - mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), nil).Once() - - // Mock DA client reports latest DA height of 100 - mockDAClient := testmocks.NewMockClient(t) - mockDAClient.EXPECT().GetLatestDAHeight(mock.Anything).Return(uint64(100), nil).Maybe() - - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() - - s := NewSyncer( - st, - mockExec, - mockDAClient, - cm, - common.NopMetrics(), - cfg, - gen, - mockHeaderStore, - mockDataStore, - zerolog.Nop(), - common.DefaultBlockOptions(), - make(chan error, 1), - nil, - ) - require.NoError(t, s.initializeState()) - s.ctx = context.Background() - s.daRetriever = NewDARetriever(nil, cm, gen, zerolog.Nop()) - s.daFollower = NewDAFollower(DAFollowerConfig{ - Retriever: s.daRetriever, - Logger: zerolog.Nop(), - EventSink: common.EventSinkFunc(func(_ context.Context, _ common.DAHeightEvent) error { return nil }), - Namespace: []byte("ns"), - StartDAHeight: 0, - }) - - // Set store height to 1 so event at height 2 is "next" - batch, err := st.NewBatch(context.Background()) - require.NoError(t, err) - require.NoError(t, batch.SetHeight(1)) - require.NoError(t, batch.Commit()) - - // Send a valid P2P hint at height 50, which is below the latest DA height of 100 - evt := common.DAHeightEvent{ - Header: &types.SignedHeader{Header: types.Header{BaseHeader: types.BaseHeader{ChainID: "c", Height: 2}}}, - Data: &types.Data{Metadata: &types.Metadata{ChainID: "c", Height: 2}}, - Source: common.SourceP2P, - DaHeightHints: [2]uint64{50, 50}, - } - - s.processHeightEvent(t.Context(), &evt) - - // Verify that the priority height was queued — the hint is valid - priorityHeight := s.daFollower.(*daFollower).popPriorityHeight() - assert.Equal(t, uint64(50), priorityHeight, "valid DA hint should be queued") -} - -// TestProcessHeightEvent_SkipsDAHintWhenAlreadyDAIncluded verifies that when the -// DA-inclusion cache already has an entry for the block height carried by a P2P -// event, the DA height hint is NOT queued for priority retrieval. -func TestProcessHeightEvent_SkipsDAHintWhenAlreadyDAIncluded(t *testing.T) { - ds := dssync.MutexWrap(datastore.NewMapDatastore()) - st := store.New(ds) - cm, err := cache.NewManager(config.DefaultConfig(), st, zerolog.Nop()) - require.NoError(t, err) - - addr, _, _ := buildSyncTestSigner(t) - cfg := config.DefaultConfig() - gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} - - mockExec := testmocks.NewMockExecutor(t) - mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), nil).Once() - - mockDAClient := testmocks.NewMockClient(t) - - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() - - s := NewSyncer( - st, - mockExec, - mockDAClient, - cm, - common.NopMetrics(), - cfg, - gen, - mockHeaderStore, - mockDataStore, - zerolog.Nop(), - common.DefaultBlockOptions(), - make(chan error, 1), - nil, - ) - require.NoError(t, s.initializeState()) - s.ctx = context.Background() - s.daRetriever = NewDARetriever(nil, cm, gen, zerolog.Nop()) - s.daFollower = NewDAFollower(DAFollowerConfig{ - Retriever: s.daRetriever, - Logger: zerolog.Nop(), - EventSink: common.EventSinkFunc(func(_ context.Context, _ common.DAHeightEvent) error { return nil }), - Namespace: []byte("ns"), - StartDAHeight: 0, - }) - - // Set the store height to 1 so the event at height 2 is "next". - batch, err := st.NewBatch(context.Background()) - require.NoError(t, err) - require.NoError(t, batch.SetHeight(1)) - require.NoError(t, batch.Commit()) - - // Simulate already DA-included header and data at height 2. - cm.SetHeaderDAIncluded("somehash-hdr2", 42, 2) - cm.SetDataDAIncluded("somehash-data2", 42, 2) - - // Both hints point to DA height 100. They should be skipped due to cache hits. - evt := common.DAHeightEvent{ - Header: &types.SignedHeader{Header: types.Header{BaseHeader: types.BaseHeader{ChainID: gen.ChainID, Height: 2}}}, - Data: &types.Data{Metadata: &types.Metadata{ChainID: gen.ChainID, Height: 2}}, - Source: common.SourceP2P, - DaHeightHints: [2]uint64{100, 100}, - } - s.processHeightEvent(t.Context(), &evt) - - priorityHeight := s.daFollower.(*daFollower).popPriorityHeight() - assert.Equal(t, uint64(0), priorityHeight, - "DA hint must not be queued when header and data are already DA-included in cache") - - // Partial case: only header is DA-included at height 3, data is not. - cm2, err := cache.NewManager(config.DefaultConfig(), st, zerolog.Nop()) - require.NoError(t, err) - s.cache = cm2 - cm2.SetHeaderDAIncluded("somehash-hdr3", 55, 3) - - batch, err = st.NewBatch(context.Background()) - require.NoError(t, err) - require.NoError(t, batch.SetHeight(2)) - require.NoError(t, batch.Commit()) - - evt3 := common.DAHeightEvent{ - Header: &types.SignedHeader{Header: types.Header{BaseHeader: types.BaseHeader{ChainID: gen.ChainID, Height: 3}}}, - Data: &types.Data{Metadata: &types.Metadata{ChainID: gen.ChainID, Height: 3}, Txs: types.Txs{types.Tx("tx2")}}, - Source: common.SourceP2P, - DaHeightHints: [2]uint64{150, 151}, - } - s.processHeightEvent(t.Context(), &evt3) - - priorityHeight = s.daFollower.(*daFollower).popPriorityHeight() - assert.Equal(t, uint64(151), priorityHeight, - "data hint must be queued when only the header is already DA-included") - assert.Equal(t, uint64(0), s.daFollower.(*daFollower).popPriorityHeight(), - "no further hints should be queued") -} - -func TestProcessHeightEvent_SkipsDAHintWhenBelowRetrieverCursor(t *testing.T) { - ds := dssync.MutexWrap(datastore.NewMapDatastore()) - st := store.New(ds) - cm, err := cache.NewManager(config.DefaultConfig(), st, zerolog.Nop()) - require.NoError(t, err) - - addr, _, _ := buildSyncTestSigner(t) - cfg := config.DefaultConfig() - gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} - - mockExec := testmocks.NewMockExecutor(t) - mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), nil).Once() - - // Mock DA client reports latest DA height of 100 - mockDAClient := testmocks.NewMockClient(t) - mockDAClient.EXPECT().GetLatestDAHeight(mock.Anything).Return(uint64(100), nil).Maybe() - - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() - - s := NewSyncer( - st, - mockExec, - mockDAClient, - cm, - common.NopMetrics(), - cfg, - gen, - mockHeaderStore, - mockDataStore, - zerolog.Nop(), - common.DefaultBlockOptions(), - make(chan error, 1), - nil, - ) - require.NoError(t, s.initializeState()) - s.ctx = context.Background() - - // Create a real daRetriever to test priority queue - s.daRetriever = NewDARetriever(nil, cm, gen, zerolog.Nop()) - s.daFollower = NewDAFollower(DAFollowerConfig{ - Retriever: s.daRetriever, - Logger: zerolog.Nop(), - EventSink: common.EventSinkFunc(func(_ context.Context, _ common.DAHeightEvent) error { return nil }), - Namespace: []byte("ns"), - StartDAHeight: 0, - }) - - // Set DA retriever height to 150 - simulating we've already fetched past height 100 - s.daRetrieverHeight.Store(150) - - // Set the store height to 1 so the event can be processed - batch, err := st.NewBatch(context.Background()) - require.NoError(t, err) - require.NoError(t, batch.SetHeight(1)) - require.NoError(t, batch.Commit()) - - // Create event with DA height hint that is BELOW the current daRetrieverHeight - evt := common.DAHeightEvent{ - Header: &types.SignedHeader{Header: types.Header{BaseHeader: types.BaseHeader{ChainID: "c", Height: 2}}}, - Data: &types.Data{Metadata: &types.Metadata{ChainID: "c", Height: 2}}, - Source: common.SourceP2P, - DaHeightHints: [2]uint64{100, 100}, // Both hints are below 150 - } - - s.processHeightEvent(t.Context(), &evt) - - // Verify that no priority height was queued since we've already fetched past it - priorityHeight := s.daFollower.(*daFollower).popPriorityHeight() - assert.Equal(t, uint64(0), priorityHeight, "should not queue DA hint that is below current daRetrieverHeight") - - // Now test with a hint that is ABOVE the current daRetrieverHeight - evt2 := common.DAHeightEvent{ - Header: &types.SignedHeader{Header: types.Header{BaseHeader: types.BaseHeader{ChainID: "c", Height: 3}}}, - Data: &types.Data{Metadata: &types.Metadata{ChainID: "c", Height: 3}}, - Source: common.SourceP2P, - DaHeightHints: [2]uint64{200, 200}, // Both hints are above 150 - } - - // Set the store height to 2 so the event can be processed - batch, err = st.NewBatch(context.Background()) - require.NoError(t, err) - require.NoError(t, batch.SetHeight(2)) - require.NoError(t, batch.Commit()) - - s.processHeightEvent(t.Context(), &evt2) - - // Verify that the priority height WAS queued since it's above daRetrieverHeight - priorityHeight = s.daFollower.(*daFollower).popPriorityHeight() - assert.Equal(t, uint64(200), priorityHeight, "should queue DA hint that is above current daRetrieverHeight") -} - // TestProcessHeightEvent_ExecutionFailure_DoesNotReschedule verifies that when // ExecuteTxs fails after all retries (execution client unavailable), the event // is NOT re-queued as pending. @@ -1453,11 +1010,6 @@ func TestProcessHeightEvent_ExecutionFailure_DoesNotReschedule(t *testing.T) { mockExec.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(1), mock.Anything, mock.Anything). Return([]byte(nil), errors.New("connection refused")).Times(common.MaxRetriesBeforeHalt) - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() - errChan := make(chan error, 1) s := NewSyncer( st, @@ -1467,8 +1019,6 @@ func TestProcessHeightEvent_ExecutionFailure_DoesNotReschedule(t *testing.T) { common.NopMetrics(), cfg, gen, - mockHeaderStore, - mockDataStore, zerolog.Nop(), common.DefaultBlockOptions(), errChan, @@ -1520,11 +1070,6 @@ func TestSyncer_Stop_SkipsDrainOnCriticalError(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockExec.EXPECT().InitChain(mock.Anything, mock.Anything, uint64(1), "tchain").Return([]byte("app0"), nil).Once() - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() - errChan := make(chan error, 1) s := NewSyncer( st, @@ -1534,8 +1079,6 @@ func TestSyncer_Stop_SkipsDrainOnCriticalError(t *testing.T) { common.NopMetrics(), cfg, gen, - mockHeaderStore, - mockDataStore, zerolog.Nop(), common.DefaultBlockOptions(), errChan, @@ -1600,11 +1143,6 @@ func TestSyncer_Stop_DrainWorksWithoutCriticalError(t *testing.T) { mockExec.EXPECT().ExecuteTxs(mock.Anything, mock.Anything, uint64(1), mock.Anything, mock.Anything). Return([]byte("app1"), nil).Once() - mockHeaderStore := extmocks.NewMockStore[*types.P2PSignedHeader](t) - mockHeaderStore.EXPECT().Height().Return(uint64(0)).Maybe() - mockDataStore := extmocks.NewMockStore[*types.P2PData](t) - mockDataStore.EXPECT().Height().Return(uint64(0)).Maybe() - errChan := make(chan error, 1) s := NewSyncer( st, @@ -1614,8 +1152,6 @@ func TestSyncer_Stop_DrainWorksWithoutCriticalError(t *testing.T) { common.NopMetrics(), cfg, gen, - mockHeaderStore, - mockDataStore, zerolog.Nop(), common.DefaultBlockOptions(), errChan, diff --git a/node/failover.go b/node/failover.go index 42dac4e8bc..ecf0641e6a 100644 --- a/node/failover.go +++ b/node/failover.go @@ -62,10 +62,6 @@ func newSyncMode( rktStore, exec, da, - headerSyncService.Store(), - dataSyncService.Store(), - headerSyncService, - dataSyncService, logger, blockMetrics, nodeOpts.BlockOptions, @@ -98,8 +94,6 @@ func newAggregatorMode( sequencer, da, signer, - headerSyncService, - dataSyncService, logger, blockMetrics, nodeOpts.BlockOptions, diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 54e58fba97..64ceea469e 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -70,28 +70,28 @@ func TestAddFlags(t *testing.T) { assertFlagValue(t, flags, FlagReadinessMaxBlocksBehind, DefaultConfig().Node.ReadinessMaxBlocksBehind) assertFlagValue(t, flags, FlagScrapeInterval, DefaultConfig().Node.ScrapeInterval) - // DA flags - assertFlagValue(t, flags, FlagDAAddress, DefaultConfig().DA.Address) - assertFlagValue(t, flags, FlagDAAuthToken, DefaultConfig().DA.AuthToken) - assertFlagValue(t, flags, FlagDABlockTime, DefaultConfig().DA.BlockTime.Duration) - assertFlagValue(t, flags, FlagDANamespace, DefaultConfig().DA.Namespace) - assertFlagValue(t, flags, FlagDADataNamespace, DefaultConfig().DA.DataNamespace) - assertFlagValue(t, flags, FlagDAForcedInclusionNamespace, DefaultConfig().DA.ForcedInclusionNamespace) - assertFlagValue(t, flags, FlagDASubmitOptions, DefaultConfig().DA.SubmitOptions) - assertFlagValue(t, flags, FlagDASigningAddresses, DefaultConfig().DA.SigningAddresses) - assertFlagValue(t, flags, FlagDAMempoolTTL, DefaultConfig().DA.MempoolTTL) - assertFlagValue(t, flags, FlagDAMaxSubmitAttempts, DefaultConfig().DA.MaxSubmitAttempts) - assertFlagValue(t, flags, FlagDARequestTimeout, DefaultConfig().DA.RequestTimeout.Duration) - assertFlagValue(t, flags, FlagDABatchingStrategy, DefaultConfig().DA.BatchingStrategy) - assertFlagValue(t, flags, FlagDABatchSizeThreshold, DefaultConfig().DA.BatchSizeThreshold) - assertFlagValue(t, flags, FlagDABatchMaxDelay, DefaultConfig().DA.BatchMaxDelay.Duration) - assertFlagValue(t, flags, FlagDABatchMinItems, DefaultConfig().DA.BatchMinItems) - - // DA Fiber flags - assertFlagValue(t, flags, FlagDAFiberEnabled, DefaultConfig().DA.Fiber.Enabled) - assertFlagValue(t, flags, FlagDAFiberConsensusAddress, DefaultConfig().DA.Fiber.ConsensusAddress) - assertFlagValue(t, flags, FlagDAFiberConsensusChainID, DefaultConfig().DA.Fiber.ConsensusChainID) - assertFlagValue(t, flags, FlagDAFiberKeyName, DefaultConfig().DA.Fiber.KeyName) + // DA flags + assertFlagValue(t, flags, FlagDAAddress, DefaultConfig().DA.Address) + assertFlagValue(t, flags, FlagDAAuthToken, DefaultConfig().DA.AuthToken) + assertFlagValue(t, flags, FlagDABlockTime, DefaultConfig().DA.BlockTime.Duration) + assertFlagValue(t, flags, FlagDANamespace, DefaultConfig().DA.Namespace) + assertFlagValue(t, flags, FlagDADataNamespace, DefaultConfig().DA.DataNamespace) + assertFlagValue(t, flags, FlagDAForcedInclusionNamespace, DefaultConfig().DA.ForcedInclusionNamespace) + assertFlagValue(t, flags, FlagDASubmitOptions, DefaultConfig().DA.SubmitOptions) + assertFlagValue(t, flags, FlagDASigningAddresses, DefaultConfig().DA.SigningAddresses) + assertFlagValue(t, flags, FlagDAMempoolTTL, DefaultConfig().DA.MempoolTTL) + assertFlagValue(t, flags, FlagDAMaxSubmitAttempts, DefaultConfig().DA.MaxSubmitAttempts) + assertFlagValue(t, flags, FlagDARequestTimeout, DefaultConfig().DA.RequestTimeout.Duration) + assertFlagValue(t, flags, FlagDABatchingStrategy, DefaultConfig().DA.BatchingStrategy) + assertFlagValue(t, flags, FlagDABatchSizeThreshold, DefaultConfig().DA.BatchSizeThreshold) + assertFlagValue(t, flags, FlagDABatchMaxDelay, DefaultConfig().DA.BatchMaxDelay.Duration) + assertFlagValue(t, flags, FlagDABatchMinItems, DefaultConfig().DA.BatchMinItems) + + // DA Fiber flags + assertFlagValue(t, flags, FlagDAFiberEnabled, DefaultConfig().DA.Fiber.Enabled) + assertFlagValue(t, flags, FlagDAFiberConsensusAddress, DefaultConfig().DA.Fiber.ConsensusAddress) + assertFlagValue(t, flags, FlagDAFiberConsensusChainID, DefaultConfig().DA.Fiber.ConsensusChainID) + assertFlagValue(t, flags, FlagDAFiberKeyName, DefaultConfig().DA.Fiber.KeyName) // P2P flags assertFlagValue(t, flags, FlagP2PListenAddress, DefaultConfig().P2P.ListenAddress) @@ -157,8 +157,8 @@ func TestAddFlags(t *testing.T) { assertFlagValue(t, flags, FlagPruningKeepRecent, DefaultConfig().Pruning.KeepRecent) assertFlagValue(t, flags, FlagPruningInterval, DefaultConfig().Pruning.Interval.Duration) - // Count the number of flags we're explicitly checking - expectedFlagCount := 84 // Update this number if you add more flag checks above + // Count the number of flags we're explicitly checking + expectedFlagCount := 84 // Update this number if you add more flag checks above // Get the actual number of flags (both regular and persistent) actualFlagCount := 0 diff --git a/test/e2e/go.mod b/test/e2e/go.mod index ba3330be20..4d8ee14fa2 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -87,7 +87,6 @@ require ( github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/celestiaorg/go-header v0.8.5 // indirect - github.com/celestiaorg/go-libp2p-messenger v0.2.2 // indirect github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3 // indirect github.com/celestiaorg/nmt v0.24.3 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect @@ -162,6 +161,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/google/orderedcode v0.0.1 // indirect + github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect @@ -245,6 +245,8 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/oklog/run v1.1.0 // indirect + github.com/onsi/ginkgo/v2 v2.23.3 // indirect + github.com/onsi/gomega v1.36.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect diff --git a/test/e2e/go.sum b/test/e2e/go.sum index b5f45e1d94..d045ab877d 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -223,8 +223,6 @@ github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3/go.mod github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/celestiaorg/go-header v0.8.5 h1:MkzlioiSeybKVNDa0805fS3mS3NG8ub93Gs2xaKwSZ4= github.com/celestiaorg/go-header v0.8.5/go.mod h1:DKl6pcKCJ0ehGUgDmfxBNz6Lv0Ky4E1Oyrcx96eQm/4= -github.com/celestiaorg/go-libp2p-messenger v0.2.2 h1:osoUfqjss7vWTIZrrDSy953RjQz+ps/vBFE7bychLEc= -github.com/celestiaorg/go-libp2p-messenger v0.2.2/go.mod h1:oTCRV5TfdO7V/k6nkx7QjQzGrWuJbupv+0o1cgnY2i4= github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3 h1:wP84mtwOCVNOTfS3zErICjxKLnh74Z1uf+tdrlSFjVM= github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3/go.mod h1:86qIYnEhmn/hfW+xvw98NOI3zGaDEB3x8JGjYo2FqLs= github.com/celestiaorg/go-square/v3 v3.0.2 h1:eSQOgNII8inK9IhiBZ+6GADQeWbRq4HYY72BOgcduA4= @@ -958,8 +956,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= diff --git a/tools/celestia-node-fiber/go.mod b/tools/celestia-node-fiber/go.mod index 569776c998..fc3069e10f 100644 --- a/tools/celestia-node-fiber/go.mod +++ b/tools/celestia-node-fiber/go.mod @@ -45,7 +45,7 @@ require ( github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 go.uber.org/fx v1.24.0 - google.golang.org/grpc v1.80.0 + google.golang.org/grpc v1.81.0 ) require ( @@ -55,7 +55,7 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.7.0 // indirect - cloud.google.com/go/kms v1.29.0 // indirect + cloud.google.com/go/kms v1.30.0 // indirect cloud.google.com/go/longrunning v0.9.0 // indirect cloud.google.com/go/monitoring v1.24.3 // indirect cloud.google.com/go/storage v1.61.3 // indirect @@ -91,24 +91,24 @@ require ( github.com/RaduBerinde/axisds v0.1.0 // indirect github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54 // indirect github.com/armon/go-metrics v0.4.1 // indirect - github.com/aws/aws-sdk-go-v2 v1.41.6 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 // indirect - github.com/aws/aws-sdk-go-v2/config v1.32.16 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.15 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.17 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 // indirect - github.com/aws/aws-sdk-go-v2/service/kms v1.51.0 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.51.1 // indirect github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect github.com/aws/smithy-go v1.25.1 // indirect github.com/bcp-innovations/hyperlane-cosmos v1.1.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect @@ -130,7 +130,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/readline v1.5.1 // indirect - github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/cockroachdb/apd/v2 v2.0.2 // indirect github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b // indirect github.com/cockroachdb/errors v1.12.0 // indirect @@ -168,8 +168,8 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect github.com/emicklei/dot v1.6.2 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/ethereum/go-ethereum v1.17.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -206,8 +206,8 @@ require ( github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.21.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect + github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect @@ -369,7 +369,7 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0 // indirect @@ -386,7 +386,7 @@ require ( go.uber.org/dig v1.19.0 // indirect go.uber.org/mock v0.5.2 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.1 // indirect + go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.50.0 // indirect @@ -403,10 +403,10 @@ require ( golang.org/x/tools v0.43.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gonum.org/v1/gonum v0.17.0 // indirect - google.golang.org/api v0.276.0 // indirect + google.golang.org/api v0.277.0 // indirect google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/tools/celestia-node-fiber/go.sum b/tools/celestia-node-fiber/go.sum index 17ac7a1de7..4ab7f345f0 100644 --- a/tools/celestia-node-fiber/go.sum +++ b/tools/celestia-node-fiber/go.sum @@ -39,8 +39,8 @@ cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1 cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U= cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY= -cloud.google.com/go/kms v1.29.0 h1:bAW1C5FQf+6GhPkywQzPlsULALCG7c16qpXLFGV9ivY= -cloud.google.com/go/kms v1.29.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U= +cloud.google.com/go/kms v1.30.0 h1:TSEZopy/OXojbVCQS9Rrx0jFqdKWvO31+N8ncXbxG/s= +cloud.google.com/go/kms v1.30.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U= cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY= @@ -157,42 +157,42 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg= -github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ= +github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= +github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 h1:adBsCIIpLbLmYnkQU+nAChU5yhVTvu5PerROm+/Kq2A= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9/go.mod h1:uOYhgfgThm/ZyAuJGNQ5YgNyOlYfqnGpTHXvk3cpykg= -github.com/aws/aws-sdk-go-v2/config v1.32.16 h1:Q0iQ7quUgJP0F/SCRTieScnaMdXr9h/2+wze1u3cNeM= -github.com/aws/aws-sdk-go-v2/config v1.32.16/go.mod h1:duCCnJEFqpt2RC6no1iK6q+8HpwOAkiUua0pY507dQc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.15 h1:fyvgWTszojq8hEnMi8PPBTvZdTtEVmAVyo+NFLHBhH4= -github.com/aws/aws-sdk-go-v2/credentials v1.19.15/go.mod h1:gJiYyMOjNg8OEdRWOf3CrFQxM2a98qmrtjx1zuiQfB8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 h1:IOGsJ1xVWhsi+ZO7/NW8OuZZBtMJLZbk4P5HDjJO0jQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22/go.mod h1:b+hYdbU+jGKfXE8kKM6g1+h+L/Go3vMvzlxBsiuGsxg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 h1:GmLa5Kw1ESqtFpXsx5MmC84QWa/ZrLZvlJGa2y+4kcQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22/go.mod h1:6sW9iWm9DK9YRpRGga/qzrzNLgKpT2cIxb7Vo2eNOp0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 h1:dY4kWZiSaXIzxnKlj17nHnBcXXBfac6UlsAx2qL6XrU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22/go.mod h1:KIpEUx0JuRZLO7U6cbV204cWAEco2iC3l061IxlwLtI= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 h1:FPXsW9+gMuIeKmz7j6ENWcWtBGTe1kH8r9thNt5Uxx4= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23/go.mod h1:7J8iGMdRKk6lw2C+cMIphgAnT8uTwBwNOsGkyOCm80U= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 h1:HtOTYcbVcGABLOVuPYaIihj6IlkqubBwFj10K5fxRek= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8/go.mod h1:VsK9abqQeGlzPgUr+isNWzPlK2vKe9INMLWnY65f5Xs= +github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= +github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 h1:xnvDEnw+pnj5mctWiYuFbigrEzSm35x7k4KS/ZkCANg= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14/go.mod h1:yS5rNogD8e0Wu9+l3MUwr6eENBzEeGejvINpN5PAYfY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 h1:PUmZeJU6Y1Lbvt9WFuJ0ugUK2xn6hIWUBBbKuOWF30s= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22/go.mod h1:nO6egFBoAaoXze24a2C0NjQCvdpk8OueRoYimvEB9jo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 h1:SE+aQ4DEqG53RRCAIHlCf//B2ycxGH7jFkpnAh/kKPM= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22/go.mod h1:ES3ynECd7fYeJIL6+oax+uIEljmfps0S70BaQzbMd/o= -github.com/aws/aws-sdk-go-v2/service/kms v1.51.0 h1:696UM+NwOrETBCLQJyCAGtVmmZmziBT59yMwgg6Fvrw= -github.com/aws/aws-sdk-go-v2/service/kms v1.51.0/go.mod h1:GBO/aaEi47QldDVoqw2CsM2UZQDoqDiFIMJD/ztHPs0= +github.com/aws/aws-sdk-go-v2/service/kms v1.51.1 h1:zuSf4olLKZW8cF/W9Y5wvGT+/0raY/3kVp49KsGs0QY= +github.com/aws/aws-sdk-go-v2/service/kms v1.51.1/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI= github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1 h1:kU/eBN5+MWNo/LcbNa4hWDdN76hdcd7hocU5kvu7IsU= github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1/go.mod h1:Fw9aqhJicIVee1VytBBjH+l+5ov6/PhbtIK/u3rt/ls= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 h1:a1Fq/KXn75wSzoJaPQTgZO0wHGqE9mjFnylnqEPTchA= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.10/go.mod h1:p6+MXNxW7IA6dMgHfTAzljuwSKD0NCm/4lbS4t6+7vI= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 h1:x6bKbmDhsgSZwv6q19wY/u3rLk/3FGjJWyqKcIRufpE= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.16/go.mod h1:CudnEVKRtLn0+3uMV0yEXZ+YZOKnAtUJ5DmDhilVnIw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 h1:oK/njaL8GtyEihkWMD4k3VgHCT64RQKkZwh0DG5j8ak= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20/go.mod h1:JHs8/y1f3zY7U5WcuzoJ/yAYGYtNIVPKLIbp61euvmg= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 h1:ks8KBcZPh3PYISr5dAiXCM5/Thcuxk8l+PG4+A0exds= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.0/go.mod h1:pFw33T0WLvXU3rw1WBkpMlkgIn54eCB5FYLhjDc9Foo= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/bcp-innovations/hyperlane-cosmos v1.1.0 h1:WXt+WrKv2DG/xVIkLvggDRbi/2law104Vj6AWZGxHNw= @@ -297,8 +297,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= -github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b h1:SHlYZ/bMx7frnmeqCu+xm0TCxXLzX3jQIVuFbnFGtFU= @@ -425,13 +425,13 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= -github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= -github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= -github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes= github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o= github.com/evstack/ev-node/core v1.0.0 h1:s0Tx0uWHme7SJn/ZNEtee4qNM8UO6PIxXnHhPbbKTz8= @@ -629,12 +629,12 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= -github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= +github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI= -github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= +github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= +github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= @@ -1303,8 +1303,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk= go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4= -go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= -go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= @@ -1361,8 +1361,8 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -1735,8 +1735,8 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/api v0.276.0 h1:nVArUtfLEihtW+b0DdcqRGK1xoEm2+ltAihyztq7MKY= -google.golang.org/api v0.276.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw= +google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk= +google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1796,8 +1796,8 @@ google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgn google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -1826,8 +1826,8 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=