From dab3be3bdaaeb49f1b8ede4c2f3cc09957bdf956 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Thu, 9 Apr 2026 11:08:03 +0200 Subject: [PATCH 1/9] feat: solo sequencer --- apps/testapp/go.mod | 2 +- apps/testapp/go.sum | 2 - pkg/sequencers/solo/README.md | 90 +++++++++ pkg/sequencers/solo/sequencer.go | 156 +++++++++++++++ pkg/sequencers/solo/sequencer_test.go | 270 ++++++++++++++++++++++++++ test/e2e/failover_e2e_test.go | 2 - 6 files changed, 517 insertions(+), 5 deletions(-) create mode 100644 pkg/sequencers/solo/README.md create mode 100644 pkg/sequencers/solo/sequencer.go create mode 100644 pkg/sequencers/solo/sequencer_test.go diff --git a/apps/testapp/go.mod b/apps/testapp/go.mod index d80110b3fc..039ba01b9f 100644 --- a/apps/testapp/go.mod +++ b/apps/testapp/go.mod @@ -2,7 +2,7 @@ module github.com/evstack/ev-node/apps/testapp go 1.25.7 -// replace github.com/evstack/ev-node => ../../. +replace github.com/evstack/ev-node => ../../. require ( github.com/evstack/ev-node v1.1.0-rc.2 diff --git a/apps/testapp/go.sum b/apps/testapp/go.sum index 8350f001ff..2895f9598a 100644 --- a/apps/testapp/go.sum +++ b/apps/testapp/go.sum @@ -432,8 +432,6 @@ github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87K github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= 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/evstack/ev-node v1.1.0-rc.2 h1:7fdGpzjJwtNLtLJJ/Fyj2yFv5ARKnMAPh22Z5cRd1r0= -github.com/evstack/ev-node v1.1.0-rc.2/go.mod h1:5lIACV0hQGO5Btdb1b3fSw2Vz7Jvrg2yvMefalfWguA= github.com/evstack/ev-node/core v1.0.0 h1:s0Tx0uWHme7SJn/ZNEtee4qNM8UO6PIxXnHhPbbKTz8= github.com/evstack/ev-node/core v1.0.0/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= diff --git a/pkg/sequencers/solo/README.md b/pkg/sequencers/solo/README.md new file mode 100644 index 0000000000..7f2670d51a --- /dev/null +++ b/pkg/sequencers/solo/README.md @@ -0,0 +1,90 @@ +# Solo Sequencer + +A minimal single-leader sequencer without forced inclusion support. It accepts mempool transactions via an in-memory queue and produces batches on demand. + +## Overview + +The solo sequencer is the simplest sequencer implementation. It has no DA-layer interaction for transaction ordering and no crash-recovery persistence. Transactions are held in memory and lost on restart. + +Use it when you need a single node that orders transactions without the overhead of forced inclusion checkpoints or DA-based sequencing. + +```mermaid +flowchart LR + Client["Client"] -->|SubmitBatchTxs| Sequencer["SoloSequencer"] + Sequencer -->|GetNextBatch| BlockManager["Block Manager"] +``` + +## Design Decisions + +| Decision | Rationale | +|---|---| +| In-memory queue | No persistence overhead; suitable for trusted single-operator setups | +| No forced inclusion | Avoids DA epoch tracking, checkpoint storage, and catch-up logic | +| No DA client dependency | `VerifyBatch` returns true unconditionally | +| Configurable queue limit | Provides backpressure when blocks can't be produced fast enough | + +## Flow + +### SubmitBatchTxs + +```mermaid +flowchart TD + A["SubmitBatchTxs()"] --> B{"Valid ID?"} + B -->|No| C["Return ErrInvalidID"] + B -->|Yes| D{"Empty batch?"} + D -->|Yes| E["Return OK"] + D -->|No| F{"Queue full?"} + F -->|Yes| G["Return ErrQueueFull"] + F -->|No| H["Append txs to queue"] + H --> E +``` + +### GetNextBatch + +```mermaid +flowchart TD + A["GetNextBatch()"] --> B{"Valid ID?"} + B -->|No| C["Return ErrInvalidID"] + B -->|Yes| D["Drain queue"] + D --> E{"Queue was empty?"} + E -->|Yes| F["Return empty batch"] + E -->|No| G["FilterTxs via executor"] + G --> H["Re-queue postponed txs"] + H --> I["Return valid txs"] +``` + +## Usage + +```go +seq := solo.NewSoloSequencer( + logger, + cfg, + []byte("chain-id"), + 1000, // maxQueueSize (0 = unlimited) + genesis, + executor, +) + +// Submit transactions from the mempool +seq.SubmitBatchTxs(ctx, coresequencer.SubmitBatchTxsRequest{ + Id: []byte("chain-id"), + Batch: &coresequencer.Batch{Transactions: txs}, +}) + +// Produce the next block +resp, err := seq.GetNextBatch(ctx, coresequencer.GetNextBatchRequest{ + Id: []byte("chain-id"), + MaxBytes: 500_000, +}) +``` + +## Comparison with Other Sequencers + +| Aspect | Solo | Single | Based | +|---|---|---|---| +| Mempool transactions | Yes | Yes | No | +| Forced inclusion | No | Yes | Yes | +| Persistence | None | DB-backed queue + checkpoints | Checkpoints only | +| Crash recovery | Lost on restart | Full recovery | Checkpoint-based | +| Catch-up mode | N/A | Yes | N/A | +| DA client required | No | Yes | Yes | diff --git a/pkg/sequencers/solo/sequencer.go b/pkg/sequencers/solo/sequencer.go new file mode 100644 index 0000000000..633466905d --- /dev/null +++ b/pkg/sequencers/solo/sequencer.go @@ -0,0 +1,156 @@ +package solo + +import ( + "bytes" + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/rs/zerolog" + + "github.com/evstack/ev-node/core/execution" + coresequencer "github.com/evstack/ev-node/core/sequencer" + "github.com/evstack/ev-node/pkg/config" + "github.com/evstack/ev-node/pkg/genesis" +) + +var ( + ErrInvalidID = errors.New("invalid chain id") + ErrQueueFull = errors.New("transaction queue is full") +) + +var _ coresequencer.Sequencer = (*SoloSequencer)(nil) + +// SoloSequencer is a single-leader sequencer without forced inclusion +// support. It maintains a simple in-memory queue of mempool transactions and +// produces batches on demand. +type SoloSequencer struct { + logger zerolog.Logger + genesis genesis.Genesis + id []byte + executor execution.Executor + + daHeight atomic.Uint64 + + mu sync.Mutex + queue [][]byte + maxQueueSize int +} + +func NewSoloSequencer( + logger zerolog.Logger, + cfg config.Config, + id []byte, + maxQueueSize int, + genesis genesis.Genesis, + executor execution.Executor, +) *SoloSequencer { + return &SoloSequencer{ + logger: logger, + genesis: genesis, + id: id, + executor: executor, + queue: make([][]byte, 0), + maxQueueSize: maxQueueSize, + } +} + +func (s *SoloSequencer) isValid(id []byte) bool { + return bytes.Equal(s.id, id) +} + +func (s *SoloSequencer) SubmitBatchTxs(ctx context.Context, req coresequencer.SubmitBatchTxsRequest) (*coresequencer.SubmitBatchTxsResponse, error) { + if !s.isValid(req.Id) { + return nil, ErrInvalidID + } + + if req.Batch == nil || len(req.Batch.Transactions) == 0 { + return &coresequencer.SubmitBatchTxsResponse{}, nil + } + + s.mu.Lock() + defer s.mu.Unlock() + + if s.maxQueueSize > 0 && len(s.queue)+len(req.Batch.Transactions) > s.maxQueueSize { + return nil, fmt.Errorf("%w: queue has %d txs, batch has %d txs, limit is %d", + ErrQueueFull, len(s.queue), len(req.Batch.Transactions), s.maxQueueSize) + } + + s.queue = append(s.queue, req.Batch.Transactions...) + return &coresequencer.SubmitBatchTxsResponse{}, nil +} + +func (s *SoloSequencer) GetNextBatch(ctx context.Context, req coresequencer.GetNextBatchRequest) (*coresequencer.GetNextBatchResponse, error) { + if !s.isValid(req.Id) { + return nil, ErrInvalidID + } + + s.mu.Lock() + txs := s.queue + s.queue = nil + s.mu.Unlock() + + if len(txs) == 0 { + return &coresequencer.GetNextBatchResponse{ + Batch: &coresequencer.Batch{}, + Timestamp: time.Now().UTC(), + BatchData: req.LastBatchData, + }, nil + } + + var maxGas uint64 + info, err := s.executor.GetExecutionInfo(ctx) + if err != nil { + s.logger.Warn().Err(err).Msg("failed to get execution info") + } else { + maxGas = info.MaxGas + } + + filterStatuses, err := s.executor.FilterTxs(ctx, txs, req.MaxBytes, maxGas, false) + if err != nil { + s.logger.Warn().Err(err).Msg("failed to filter transactions, proceeding with unfiltered") + filterStatuses = make([]execution.FilterStatus, len(txs)) + for i := range filterStatuses { + filterStatuses[i] = execution.FilterOK + } + } + + var validTxs [][]byte + var postponedTxs [][]byte + for i, status := range filterStatuses { + switch status { + case execution.FilterOK: + validTxs = append(validTxs, txs[i]) + case execution.FilterPostpone: + postponedTxs = append(postponedTxs, txs[i]) + case execution.FilterRemove: + } + } + + if len(postponedTxs) > 0 { + s.mu.Lock() + s.queue = append(postponedTxs, s.queue...) + s.mu.Unlock() + } + + return &coresequencer.GetNextBatchResponse{ + Batch: &coresequencer.Batch{Transactions: validTxs}, + Timestamp: time.Now().UTC(), + BatchData: req.LastBatchData, + }, nil +} + +func (s *SoloSequencer) VerifyBatch(ctx context.Context, req coresequencer.VerifyBatchRequest) (*coresequencer.VerifyBatchResponse, error) { + return &coresequencer.VerifyBatchResponse{Status: true}, nil +} + +func (s *SoloSequencer) SetDAHeight(height uint64) { + s.daHeight.Store(height) +} + +func (s *SoloSequencer) GetDAHeight() uint64 { + return s.daHeight.Load() +} diff --git a/pkg/sequencers/solo/sequencer_test.go b/pkg/sequencers/solo/sequencer_test.go new file mode 100644 index 0000000000..fb5f4a4a6e --- /dev/null +++ b/pkg/sequencers/solo/sequencer_test.go @@ -0,0 +1,270 @@ +package solo + +import ( + "context" + "testing" + "time" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/evstack/ev-node/core/execution" + coresequencer "github.com/evstack/ev-node/core/sequencer" + "github.com/evstack/ev-node/pkg/config" + "github.com/evstack/ev-node/pkg/genesis" + "github.com/evstack/ev-node/test/mocks" +) + +func createDefaultMockExecutor(t *testing.T) *mocks.MockExecutor { + mockExec := mocks.NewMockExecutor(t) + mockExec.On("GetExecutionInfo", mock.Anything).Return(execution.ExecutionInfo{MaxGas: 1000000}, nil).Maybe() + mockExec.On("FilterTxs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + func(ctx context.Context, txs [][]byte, maxBytes, maxGas uint64, hasForceIncludedTransaction bool) []execution.FilterStatus { + result := make([]execution.FilterStatus, len(txs)) + var cumulativeBytes uint64 + for i, tx := range txs { + txBytes := uint64(len(tx)) + if maxBytes > 0 && cumulativeBytes+txBytes > maxBytes { + result[i] = execution.FilterPostpone + continue + } + cumulativeBytes += txBytes + result[i] = execution.FilterOK + } + return result + }, + nil, + ).Maybe() + return mockExec +} + +func newTestSequencer(t *testing.T, maxQueueSize int) *SoloSequencer { + return NewSoloSequencer( + zerolog.Nop(), + config.DefaultConfig(), + []byte("test"), + maxQueueSize, + genesis.Genesis{ChainID: "test"}, + createDefaultMockExecutor(t), + ) +} + +func TestSoloSequencer_SubmitBatchTxs(t *testing.T) { + seq := newTestSequencer(t, 0) + + tx := []byte("transaction1") + res, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ + Id: []byte("test"), + Batch: &coresequencer.Batch{Transactions: [][]byte{tx}}, + }) + require.NoError(t, err) + require.NotNil(t, res) + + nextResp, err := seq.GetNextBatch(context.Background(), coresequencer.GetNextBatchRequest{Id: []byte("test")}) + require.NoError(t, err) + require.Len(t, nextResp.Batch.Transactions, 1) + assert.Equal(t, tx, nextResp.Batch.Transactions[0]) +} + +func TestSoloSequencer_SubmitBatchTxs_InvalidID(t *testing.T) { + seq := newTestSequencer(t, 0) + + res, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ + Id: []byte("wrong"), + Batch: &coresequencer.Batch{Transactions: [][]byte{{1}}}, + }) + assert.ErrorIs(t, err, ErrInvalidID) + assert.Nil(t, res) +} + +func TestSoloSequencer_SubmitBatchTxs_EmptyBatch(t *testing.T) { + seq := newTestSequencer(t, 0) + + res, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ + Id: []byte("test"), + Batch: &coresequencer.Batch{}, + }) + require.NoError(t, err) + require.NotNil(t, res) + + assert.Empty(t, seq.queue) + + res, err = seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ + Id: []byte("test"), + Batch: nil, + }) + require.NoError(t, err) + require.NotNil(t, res) + assert.Empty(t, seq.queue) +} + +func TestSoloSequencer_SubmitBatchTxs_QueueFull(t *testing.T) { + seq := newTestSequencer(t, 3) // max 3 txs + + batch := coresequencer.Batch{Transactions: [][]byte{{1}, {2}, {3}}} + _, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ + Id: []byte("test"), + Batch: &batch, + }) + require.NoError(t, err) + + res, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ + Id: []byte("test"), + Batch: &coresequencer.Batch{Transactions: [][]byte{{4}}}, + }) + assert.ErrorIs(t, err, ErrQueueFull) + assert.Nil(t, res) +} + +func TestSoloSequencer_GetNextBatch_EmptyQueue(t *testing.T) { + seq := newTestSequencer(t, 0) + + resp, err := seq.GetNextBatch(context.Background(), coresequencer.GetNextBatchRequest{Id: []byte("test")}) + require.NoError(t, err) + require.NotNil(t, resp) + assert.Empty(t, resp.Batch.Transactions) + assert.WithinDuration(t, time.Now(), resp.Timestamp, time.Second) +} + +func TestSoloSequencer_GetNextBatch_InvalidID(t *testing.T) { + seq := newTestSequencer(t, 0) + + res, err := seq.GetNextBatch(context.Background(), coresequencer.GetNextBatchRequest{Id: []byte("wrong")}) + assert.ErrorIs(t, err, ErrInvalidID) + assert.Nil(t, res) +} + +func TestSoloSequencer_GetNextBatch_DrainsAndFilters(t *testing.T) { + seq := newTestSequencer(t, 0) + + batch := coresequencer.Batch{Transactions: [][]byte{{1}, {2}, {3}}} + _, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ + Id: []byte("test"), + Batch: &batch, + }) + require.NoError(t, err) + + resp, err := seq.GetNextBatch(context.Background(), coresequencer.GetNextBatchRequest{Id: []byte("test")}) + require.NoError(t, err) + assert.Len(t, resp.Batch.Transactions, 3) + + assert.Empty(t, seq.queue, "queue should be drained after GetNextBatch") +} + +func TestSoloSequencer_GetNextBatch_PostponedTxsRequeued(t *testing.T) { + mockExec := mocks.NewMockExecutor(t) + mockExec.On("GetExecutionInfo", mock.Anything).Return(execution.ExecutionInfo{MaxGas: 1000000}, nil).Maybe() + mockExec.On("FilterTxs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + func(ctx context.Context, txs [][]byte, maxBytes, maxGas uint64, hasForceIncludedTransaction bool) []execution.FilterStatus { + result := make([]execution.FilterStatus, len(txs)) + for i := range txs { + if i < 2 { + result[i] = execution.FilterOK + } else { + result[i] = execution.FilterPostpone + } + } + return result + }, + nil, + ).Maybe() + + seq := NewSoloSequencer( + zerolog.Nop(), + config.DefaultConfig(), + []byte("test"), + 0, + genesis.Genesis{ChainID: "test"}, + mockExec, + ) + + batch := coresequencer.Batch{Transactions: [][]byte{{1}, {2}, {3}, {4}}} + _, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ + Id: []byte("test"), + Batch: &batch, + }) + require.NoError(t, err) + + resp, err := seq.GetNextBatch(context.Background(), coresequencer.GetNextBatchRequest{Id: []byte("test")}) + require.NoError(t, err) + assert.Len(t, resp.Batch.Transactions, 2, "first 2 txs should pass filter") + + assert.Len(t, seq.queue, 2, "postponed txs should be re-queued") + assert.Equal(t, []byte{3}, seq.queue[0]) + assert.Equal(t, []byte{4}, seq.queue[1]) +} + +func TestSoloSequencer_GetNextBatch_SubmitDuringProcessing(t *testing.T) { + seq := newTestSequencer(t, 0) + + batch := coresequencer.Batch{Transactions: [][]byte{{1}, {2}}} + _, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ + Id: []byte("test"), + Batch: &batch, + }) + require.NoError(t, err) + + resp, err := seq.GetNextBatch(context.Background(), coresequencer.GetNextBatchRequest{Id: []byte("test")}) + require.NoError(t, err) + assert.Len(t, resp.Batch.Transactions, 2) + + _, err = seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ + Id: []byte("test"), + Batch: &coresequencer.Batch{Transactions: [][]byte{{3}}}, + }) + require.NoError(t, err) + + resp, err = seq.GetNextBatch(context.Background(), coresequencer.GetNextBatchRequest{Id: []byte("test")}) + require.NoError(t, err) + assert.Len(t, resp.Batch.Transactions, 1) + assert.Equal(t, []byte{3}, resp.Batch.Transactions[0]) +} + +func TestSoloSequencer_VerifyBatch(t *testing.T) { + seq := newTestSequencer(t, 0) + + batchData := [][]byte{[]byte("batch1"), []byte("batch2")} + + res, err := seq.VerifyBatch(context.Background(), coresequencer.VerifyBatchRequest{ + Id: []byte("test"), + BatchData: batchData, + }) + assert.NoError(t, err) + assert.True(t, res.Status) +} + +func TestSoloSequencer_DAHeight(t *testing.T) { + seq := newTestSequencer(t, 0) + + assert.Equal(t, uint64(0), seq.GetDAHeight()) + + seq.SetDAHeight(42) + assert.Equal(t, uint64(42), seq.GetDAHeight()) +} + +func TestSoloSequencer_QueueFullThenFreed(t *testing.T) { + seq := newTestSequencer(t, 2) // max 2 txs + + _, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ + Id: []byte("test"), + Batch: &coresequencer.Batch{Transactions: [][]byte{{1}, {2}}}, + }) + require.NoError(t, err) + + _, err = seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ + Id: []byte("test"), + Batch: &coresequencer.Batch{Transactions: [][]byte{{3}}}, + }) + assert.ErrorIs(t, err, ErrQueueFull) + + _, err = seq.GetNextBatch(context.Background(), coresequencer.GetNextBatchRequest{Id: []byte("test")}) + require.NoError(t, err) + + _, err = seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ + Id: []byte("test"), + Batch: &coresequencer.Batch{Transactions: [][]byte{{3}}}, + }) + assert.NoError(t, err, "submission should succeed after queue is drained") +} diff --git a/test/e2e/failover_e2e_test.go b/test/e2e/failover_e2e_test.go index 1e1f53dfd1..2fc84e1b34 100644 --- a/test/e2e/failover_e2e_test.go +++ b/test/e2e/failover_e2e_test.go @@ -837,8 +837,6 @@ func submitTxToURL(t *testing.T, client *ethclient.Client) (common.Hash, uint64) return tx.Hash(), blk } -const defaultMaxBlobSize = 2 * 1024 * 1024 // 2MB - func queryLastDAHeight(t *testing.T, jwtSecret string, daAddress string) uint64 { t.Helper() blobClient, err := blobrpc.NewClient(t.Context(), daAddress, jwtSecret, "") From 1fc86a53783153f9cf5dce79eb0e40b55b61a2e6 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Thu, 9 Apr 2026 11:26:31 +0200 Subject: [PATCH 2/9] refactor: set max blob size as ldflag --- block/internal/common/consts.go | 6 +++- block/internal/da/client.go | 2 +- .../internal/submitting/batching_strategy.go | 16 ++++----- .../submitting/batching_strategy_test.go | 36 +++++++++---------- block/internal/submitting/da_submitter.go | 12 +++---- block/internal/submitting/submitter.go | 13 ++++--- 6 files changed, 43 insertions(+), 42 deletions(-) diff --git a/block/internal/common/consts.go b/block/internal/common/consts.go index 6f881d55a2..c6c2bcd7d4 100644 --- a/block/internal/common/consts.go +++ b/block/internal/common/consts.go @@ -1,3 +1,7 @@ package common -const DefaultMaxBlobSize = 5 * 1024 * 1024 // 5MB fallback blob size limit +// DefaultMaxBlobSize is the fallback blob size limit used when the DA layer +// does not report one. Override at build time with: +// +// go build -ldflags "-X github.com/evstack/ev-node/block/internal/common.DefaultMaxBlobSize=10485760" +var DefaultMaxBlobSize uint64 = 5 * 1024 * 1024 // 5MB diff --git a/block/internal/da/client.go b/block/internal/da/client.go index 4db8e7716a..35fd50b91d 100644 --- a/block/internal/da/client.go +++ b/block/internal/da/client.go @@ -559,7 +559,7 @@ func extractBlobData(resp *blobrpc.SubscriptionResponse) [][]byte { continue } data := blob.Data() - if len(data) == 0 || len(data) > common.DefaultMaxBlobSize { + if len(data) == 0 || uint64(len(data)) > common.DefaultMaxBlobSize { continue } blobs = append(blobs, data) diff --git a/block/internal/submitting/batching_strategy.go b/block/internal/submitting/batching_strategy.go index 72eccdc4d9..b71ab78fdc 100644 --- a/block/internal/submitting/batching_strategy.go +++ b/block/internal/submitting/batching_strategy.go @@ -12,7 +12,7 @@ import ( type BatchingStrategy interface { // ShouldSubmit determines if a batch should be submitted based on the strategy // Returns true if submission should happen now - ShouldSubmit(pendingCount uint64, totalSizeBeforeSig int, maxBlobSize int, timeSinceLastSubmit time.Duration) bool + ShouldSubmit(pendingCount uint64, totalSizeBeforeSig uint64, maxBlobSize uint64, timeSinceLastSubmit time.Duration) bool } // NewBatchingStrategy creates a batching strategy based on configuration @@ -34,7 +34,7 @@ func NewBatchingStrategy(cfg config.DAConfig) (BatchingStrategy, error) { // ImmediateStrategy submits as soon as any items are available type ImmediateStrategy struct{} -func (s *ImmediateStrategy) ShouldSubmit(pendingCount uint64, totalSize int, maxBlobSize int, timeSinceLastSubmit time.Duration) bool { +func (s *ImmediateStrategy) ShouldSubmit(pendingCount uint64, totalSize uint64, maxBlobSize uint64, timeSinceLastSubmit time.Duration) bool { return pendingCount > 0 } @@ -57,12 +57,12 @@ func NewSizeBasedStrategy(sizeThreshold float64, minItems uint64) *SizeBasedStra } } -func (s *SizeBasedStrategy) ShouldSubmit(pendingCount uint64, totalSize int, maxBlobSize int, timeSinceLastSubmit time.Duration) bool { +func (s *SizeBasedStrategy) ShouldSubmit(pendingCount uint64, totalSize uint64, maxBlobSize uint64, timeSinceLastSubmit time.Duration) bool { if pendingCount < s.minItems { return false } - threshold := int(float64(maxBlobSize) * s.sizeThreshold) + threshold := uint64(float64(maxBlobSize) * s.sizeThreshold) return totalSize >= threshold } @@ -85,7 +85,7 @@ func NewTimeBasedStrategy(daBlockTime time.Duration, maxDelay time.Duration, min } } -func (s *TimeBasedStrategy) ShouldSubmit(pendingCount uint64, totalSize int, maxBlobSize int, timeSinceLastSubmit time.Duration) bool { +func (s *TimeBasedStrategy) ShouldSubmit(pendingCount uint64, totalSize uint64, maxBlobSize uint64, timeSinceLastSubmit time.Duration) bool { if pendingCount < s.minItems { return false } @@ -120,18 +120,16 @@ func NewAdaptiveStrategy(daBlockTime time.Duration, sizeThreshold float64, maxDe } } -func (s *AdaptiveStrategy) ShouldSubmit(pendingCount uint64, totalSize int, maxBlobSize int, timeSinceLastSubmit time.Duration) bool { +func (s *AdaptiveStrategy) ShouldSubmit(pendingCount uint64, totalSize uint64, maxBlobSize uint64, timeSinceLastSubmit time.Duration) bool { if pendingCount < s.minItems { return false } - // Submit if we've reached the size threshold - threshold := int(float64(maxBlobSize) * s.sizeThreshold) + threshold := uint64(float64(maxBlobSize) * s.sizeThreshold) if totalSize >= threshold { return true } - // Submit if max delay has been reached if timeSinceLastSubmit >= s.maxDelay { return true } diff --git a/block/internal/submitting/batching_strategy_test.go b/block/internal/submitting/batching_strategy_test.go index f29eff0287..2246291d12 100644 --- a/block/internal/submitting/batching_strategy_test.go +++ b/block/internal/submitting/batching_strategy_test.go @@ -13,11 +13,12 @@ import ( func TestImmediateStrategy(t *testing.T) { strategy := &ImmediateStrategy{} + maxBlobSize := common.DefaultMaxBlobSize tests := []struct { name string pendingCount uint64 - totalSize int + totalSize uint64 expected bool }{ { @@ -42,7 +43,7 @@ func TestImmediateStrategy(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := strategy.ShouldSubmit(tt.pendingCount, tt.totalSize, common.DefaultMaxBlobSize, 0) + result := strategy.ShouldSubmit(tt.pendingCount, tt.totalSize, maxBlobSize, 0) assert.Equal(t, tt.expected, result) }) } @@ -56,7 +57,7 @@ func TestSizeBasedStrategy(t *testing.T) { sizeThreshold float64 minItems uint64 pendingCount uint64 - totalSize int + totalSize uint64 expectedSubmit bool }{ { @@ -80,7 +81,7 @@ func TestSizeBasedStrategy(t *testing.T) { sizeThreshold: 0.8, minItems: 1, pendingCount: 10, - totalSize: int(float64(maxBlobSize) * 0.8), // 80% of max + totalSize: uint64(float64(maxBlobSize) * 0.8), // 80% of max expectedSubmit: true, }, { @@ -88,7 +89,7 @@ func TestSizeBasedStrategy(t *testing.T) { sizeThreshold: 0.8, minItems: 1, pendingCount: 20, - totalSize: int(float64(maxBlobSize) * 0.875), // 87.5% + totalSize: uint64(float64(maxBlobSize) * 0.875), // 87.5% expectedSubmit: true, }, { @@ -125,7 +126,7 @@ func TestTimeBasedStrategy(t *testing.T) { name string minItems uint64 pendingCount uint64 - totalSize int + totalSize uint64 timeSinceLastSubmit time.Duration expectedSubmit bool }{ @@ -133,7 +134,7 @@ func TestTimeBasedStrategy(t *testing.T) { name: "below min items", minItems: 2, pendingCount: 1, - totalSize: int(float64(maxBlobSize) * 0.2), + totalSize: uint64(float64(maxBlobSize) * 0.2), timeSinceLastSubmit: 10 * time.Second, expectedSubmit: false, }, @@ -141,7 +142,7 @@ func TestTimeBasedStrategy(t *testing.T) { name: "before max delay", minItems: 1, pendingCount: 5, - totalSize: int(float64(maxBlobSize) * 0.5), + totalSize: uint64(float64(maxBlobSize) * 0.5), timeSinceLastSubmit: 3 * time.Second, expectedSubmit: false, }, @@ -149,7 +150,7 @@ func TestTimeBasedStrategy(t *testing.T) { name: "at max delay", minItems: 1, pendingCount: 3, - totalSize: int(float64(maxBlobSize) * 0.4), + totalSize: uint64(float64(maxBlobSize) * 0.4), timeSinceLastSubmit: 6 * time.Second, expectedSubmit: true, }, @@ -157,7 +158,7 @@ func TestTimeBasedStrategy(t *testing.T) { name: "after max delay", minItems: 1, pendingCount: 2, - totalSize: int(float64(maxBlobSize) * 0.2), + totalSize: uint64(float64(maxBlobSize) * 0.2), timeSinceLastSubmit: 10 * time.Second, expectedSubmit: true, }, @@ -181,7 +182,7 @@ func TestAdaptiveStrategy(t *testing.T) { name string minItems uint64 pendingCount uint64 - totalSize int + totalSize uint64 timeSinceLastSubmit time.Duration expectedSubmit bool reason string @@ -190,7 +191,7 @@ func TestAdaptiveStrategy(t *testing.T) { name: "below min items", minItems: 3, pendingCount: 2, - totalSize: int(float64(maxBlobSize) * 0.875), + totalSize: uint64(float64(maxBlobSize) * 0.875), timeSinceLastSubmit: 10 * time.Second, expectedSubmit: false, reason: "not enough items", @@ -199,7 +200,7 @@ func TestAdaptiveStrategy(t *testing.T) { name: "size threshold reached", minItems: 1, pendingCount: 10, - totalSize: int(float64(maxBlobSize) * 0.85), // 85% + totalSize: uint64(float64(maxBlobSize) * 0.85), // 85% timeSinceLastSubmit: 1 * time.Second, expectedSubmit: true, reason: "size threshold met", @@ -208,7 +209,7 @@ func TestAdaptiveStrategy(t *testing.T) { name: "time threshold reached", minItems: 1, pendingCount: 2, - totalSize: int(float64(maxBlobSize) * 0.2), // Only 20% + totalSize: uint64(float64(maxBlobSize) * 0.2), // Only 20% timeSinceLastSubmit: 7 * time.Second, expectedSubmit: true, reason: "time threshold met", @@ -217,7 +218,7 @@ func TestAdaptiveStrategy(t *testing.T) { name: "neither threshold reached", minItems: 1, pendingCount: 5, - totalSize: int(float64(maxBlobSize) * 0.5), // 50% + totalSize: uint64(float64(maxBlobSize) * 0.5), // 50% timeSinceLastSubmit: 3 * time.Second, expectedSubmit: false, reason: "waiting for threshold", @@ -226,7 +227,7 @@ func TestAdaptiveStrategy(t *testing.T) { name: "both thresholds reached", minItems: 1, pendingCount: 20, - totalSize: int(float64(maxBlobSize) * 0.875), // 87.5% + totalSize: uint64(float64(maxBlobSize) * 0.875), // 87.5% timeSinceLastSubmit: 10 * time.Second, expectedSubmit: true, reason: "both thresholds met", @@ -305,10 +306,9 @@ func TestNewBatchingStrategy(t *testing.T) { } func TestBatchingStrategiesComparison(t *testing.T) { - // This test demonstrates how different strategies behave with the same input maxBlobSize := common.DefaultMaxBlobSize pendingCount := uint64(10) - totalSize := maxBlobSize / 2 // 50% full + totalSize := maxBlobSize / 2 timeSinceLastSubmit := 3 * time.Second immediate := &ImmediateStrategy{} diff --git a/block/internal/submitting/da_submitter.go b/block/internal/submitting/da_submitter.go index 1f7edbed8d..83f56d9cb5 100644 --- a/block/internal/submitting/da_submitter.go +++ b/block/internal/submitting/da_submitter.go @@ -42,7 +42,7 @@ type retryPolicy struct { MaxAttempts int MinBackoff time.Duration MaxBackoff time.Duration - MaxBlobBytes int + MaxBlobBytes uint64 } func defaultRetryPolicy(maxAttempts int, maxDuration time.Duration) retryPolicy { @@ -581,7 +581,7 @@ func submitToDA[T any]( if err != nil { s.logger.Error(). Str("itemType", itemType). - Int("maxBlobBytes", pol.MaxBlobBytes). + Uint64("maxBlobBytes", pol.MaxBlobBytes). Err(err). Msg("CRITICAL: Unrecoverable error - item exceeds maximum blob size") return fmt.Errorf("unrecoverable error: no %s items fit within max blob size: %w", itemType, err) @@ -644,7 +644,7 @@ func submitToDA[T any]( if len(items) == 1 { s.logger.Error(). Str("itemType", itemType). - Int("maxBlobBytes", pol.MaxBlobBytes). + Uint64("maxBlobBytes", pol.MaxBlobBytes). Msg("CRITICAL: Unrecoverable error - single item exceeds DA blob size limit") return fmt.Errorf("unrecoverable error: %w: single %s item exceeds DA blob size limit", common.ErrOversizedItem, itemType) } @@ -690,11 +690,11 @@ func submitToDA[T any]( // limitBatchBySize returns a prefix of items whose total marshaled size does not exceed maxBytes. // If the first item exceeds maxBytes, it returns ErrOversizedItem which is unrecoverable. -func limitBatchBySize[T any](items []T, marshaled [][]byte, maxBytes int) ([]T, [][]byte, error) { - total := 0 +func limitBatchBySize[T any](items []T, marshaled [][]byte, maxBytes uint64) ([]T, [][]byte, error) { + total := uint64(0) count := 0 for i := range items { - sz := len(marshaled[i]) + sz := uint64(len(marshaled[i])) if sz > maxBytes { if i == 0 { return nil, nil, fmt.Errorf("%w: item size %d exceeds max %d", common.ErrOversizedItem, sz, maxBytes) diff --git a/block/internal/submitting/submitter.go b/block/internal/submitting/submitter.go index 34fab216de..da181ec1cc 100644 --- a/block/internal/submitting/submitter.go +++ b/block/internal/submitting/submitter.go @@ -201,9 +201,9 @@ func (s *Submitter) daSubmissionLoop() { } // Calculate total size (excluding signature) - totalSize := 0 + totalSize := uint64(0) for _, marshalled := range marshalledHeaders { - totalSize += len(marshalled) + totalSize += uint64(len(marshalled)) } shouldSubmit := s.batchingStrategy.ShouldSubmit( @@ -217,7 +217,7 @@ func (s *Submitter) daSubmissionLoop() { s.logger.Debug(). Time("t", time.Now()). Uint64("headers", headersNb). - Int("total_size_kb", totalSize/1024). + Uint64("total_size_kb", totalSize/1024). Dur("time_since_last", timeSinceLastSubmit). Msg("batching strategy triggered header submission") @@ -260,10 +260,9 @@ func (s *Submitter) daSubmissionLoop() { return } - // Calculate total size (excluding signature) - totalSize := 0 + totalSize := uint64(0) for _, marshalled := range marshalledData { - totalSize += len(marshalled) + totalSize += uint64(len(marshalled)) } shouldSubmit := s.batchingStrategy.ShouldSubmit( @@ -277,7 +276,7 @@ func (s *Submitter) daSubmissionLoop() { s.logger.Debug(). Time("t", time.Now()). Uint64("data", dataNb). - Int("total_size_kb", totalSize/1024). + Uint64("total_size_kb", totalSize/1024). Dur("time_since_last", timeSinceLastSubmit). Msg("batching strategy triggered data submission") From 3d84f81296735d78c570bc98741f0f2518fa384e Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Thu, 9 Apr 2026 11:38:39 +0200 Subject: [PATCH 3/9] refactor: wire solo sequencer in testapp --- apps/testapp/Dockerfile | 3 +- apps/testapp/cmd/root.go | 6 ++- apps/testapp/cmd/run.go | 9 +++- block/internal/common/consts.go | 3 +- pkg/sequencers/solo/sequencer.go | 30 +++--------- pkg/sequencers/solo/sequencer_test.go | 68 ++++----------------------- 6 files changed, 33 insertions(+), 86 deletions(-) diff --git a/apps/testapp/Dockerfile b/apps/testapp/Dockerfile index d2be1823cb..b3d9cdc021 100644 --- a/apps/testapp/Dockerfile +++ b/apps/testapp/Dockerfile @@ -27,7 +27,8 @@ RUN go mod download && (cd apps/testapp && go mod download) COPY . . WORKDIR /ev-node/apps/testapp -RUN go build -o /go/bin/testapp . + +RUN go build -ldflags "-X github.com/evstack/ev-node/block/internal/common.DefaultMaxBlobSize=125829120" -o /go/bin/testapp . ## prep the final image. # diff --git a/apps/testapp/cmd/root.go b/apps/testapp/cmd/root.go index 8e58cc794d..17db405566 100644 --- a/apps/testapp/cmd/root.go +++ b/apps/testapp/cmd/root.go @@ -12,13 +12,17 @@ const ( // flagKVEndpoint is the flag for the KV endpoint flagKVEndpoint = "kv-endpoint" + // flagSoloSequencer is the flag to enable a solo sequencer + flagSoloSequencer = "solo-sequencer" ) func init() { config.AddGlobalFlags(RootCmd, AppName) config.AddFlags(RunCmd) - // Add the KV endpoint flag specifically to the RunCmd + + // add more flags to RunCmd RunCmd.Flags().String(flagKVEndpoint, "", "Address and port for the KV executor HTTP server") + RunCmd.Flags().Bool(flagSoloSequencer, true, "Enable Solo sequencer (instead of based sequencer or single sequencer)") } // RootCmd is the root command for Evolve diff --git a/apps/testapp/cmd/run.go b/apps/testapp/cmd/run.go index ea4b0c51d0..52b01d230f 100644 --- a/apps/testapp/cmd/run.go +++ b/apps/testapp/cmd/run.go @@ -22,6 +22,7 @@ import ( "github.com/evstack/ev-node/pkg/p2p/key" "github.com/evstack/ev-node/pkg/sequencers/based" "github.com/evstack/ev-node/pkg/sequencers/single" + "github.com/evstack/ev-node/pkg/sequencers/solo" "github.com/evstack/ev-node/pkg/store" ) @@ -91,7 +92,7 @@ var RunCmd = &cobra.Command{ } // Create sequencer based on configuration - sequencer, err := createSequencer(ctx, logger, datastore, nodeConfig, genesis, executor) + sequencer, err := createSequencer(ctx, command, logger, datastore, nodeConfig, genesis, executor) if err != nil { return err } @@ -105,12 +106,18 @@ var RunCmd = &cobra.Command{ // Otherwise, it creates a single (traditional) sequencer. func createSequencer( ctx context.Context, + cmd *cobra.Command, logger zerolog.Logger, datastore datastore.Batching, nodeConfig config.Config, genesis genesis.Genesis, executor execution.Executor, ) (coresequencer.Sequencer, error) { + if enabled, _ := cmd.Flags().GetBool(flagSoloSequencer); enabled { + logger.Info().Msg("using solo sequencer") + return solo.NewSoloSequencer(logger, nodeConfig, []byte(genesis.ChainID), executor), nil + } + blobClient, err := blobrpc.NewWSClient(ctx, logger, nodeConfig.DA.Address, nodeConfig.DA.AuthToken, "") if err != nil { return nil, fmt.Errorf("failed to create blob client: %w", err) diff --git a/block/internal/common/consts.go b/block/internal/common/consts.go index c6c2bcd7d4..857ee87690 100644 --- a/block/internal/common/consts.go +++ b/block/internal/common/consts.go @@ -1,7 +1,6 @@ package common -// DefaultMaxBlobSize is the fallback blob size limit used when the DA layer -// does not report one. Override at build time with: +// DefaultMaxBlobSize is the fallback blob size limit used with the DA layer. // // go build -ldflags "-X github.com/evstack/ev-node/block/internal/common.DefaultMaxBlobSize=10485760" var DefaultMaxBlobSize uint64 = 5 * 1024 * 1024 // 5MB diff --git a/pkg/sequencers/solo/sequencer.go b/pkg/sequencers/solo/sequencer.go index 633466905d..6f56e9ef8f 100644 --- a/pkg/sequencers/solo/sequencer.go +++ b/pkg/sequencers/solo/sequencer.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "errors" - "fmt" "sync" "sync/atomic" "time" @@ -14,13 +13,9 @@ import ( "github.com/evstack/ev-node/core/execution" coresequencer "github.com/evstack/ev-node/core/sequencer" "github.com/evstack/ev-node/pkg/config" - "github.com/evstack/ev-node/pkg/genesis" ) -var ( - ErrInvalidID = errors.New("invalid chain id") - ErrQueueFull = errors.New("transaction queue is full") -) +var ErrInvalidID = errors.New("invalid chain id") var _ coresequencer.Sequencer = (*SoloSequencer)(nil) @@ -29,32 +24,26 @@ var _ coresequencer.Sequencer = (*SoloSequencer)(nil) // produces batches on demand. type SoloSequencer struct { logger zerolog.Logger - genesis genesis.Genesis id []byte executor execution.Executor daHeight atomic.Uint64 - mu sync.Mutex - queue [][]byte - maxQueueSize int + mu sync.Mutex + queue [][]byte } func NewSoloSequencer( logger zerolog.Logger, cfg config.Config, id []byte, - maxQueueSize int, - genesis genesis.Genesis, executor execution.Executor, ) *SoloSequencer { return &SoloSequencer{ - logger: logger, - genesis: genesis, - id: id, - executor: executor, - queue: make([][]byte, 0), - maxQueueSize: maxQueueSize, + logger: logger, + id: id, + executor: executor, + queue: make([][]byte, 0), } } @@ -74,11 +63,6 @@ func (s *SoloSequencer) SubmitBatchTxs(ctx context.Context, req coresequencer.Su s.mu.Lock() defer s.mu.Unlock() - if s.maxQueueSize > 0 && len(s.queue)+len(req.Batch.Transactions) > s.maxQueueSize { - return nil, fmt.Errorf("%w: queue has %d txs, batch has %d txs, limit is %d", - ErrQueueFull, len(s.queue), len(req.Batch.Transactions), s.maxQueueSize) - } - s.queue = append(s.queue, req.Batch.Transactions...) return &coresequencer.SubmitBatchTxsResponse{}, nil } diff --git a/pkg/sequencers/solo/sequencer_test.go b/pkg/sequencers/solo/sequencer_test.go index fb5f4a4a6e..f7ae0e1518 100644 --- a/pkg/sequencers/solo/sequencer_test.go +++ b/pkg/sequencers/solo/sequencer_test.go @@ -13,7 +13,6 @@ import ( "github.com/evstack/ev-node/core/execution" coresequencer "github.com/evstack/ev-node/core/sequencer" "github.com/evstack/ev-node/pkg/config" - "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/test/mocks" ) @@ -40,19 +39,17 @@ func createDefaultMockExecutor(t *testing.T) *mocks.MockExecutor { return mockExec } -func newTestSequencer(t *testing.T, maxQueueSize int) *SoloSequencer { +func newTestSequencer(t *testing.T) *SoloSequencer { return NewSoloSequencer( zerolog.Nop(), config.DefaultConfig(), []byte("test"), - maxQueueSize, - genesis.Genesis{ChainID: "test"}, createDefaultMockExecutor(t), ) } func TestSoloSequencer_SubmitBatchTxs(t *testing.T) { - seq := newTestSequencer(t, 0) + seq := newTestSequencer(t) tx := []byte("transaction1") res, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ @@ -69,7 +66,7 @@ func TestSoloSequencer_SubmitBatchTxs(t *testing.T) { } func TestSoloSequencer_SubmitBatchTxs_InvalidID(t *testing.T) { - seq := newTestSequencer(t, 0) + seq := newTestSequencer(t) res, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ Id: []byte("wrong"), @@ -80,7 +77,7 @@ func TestSoloSequencer_SubmitBatchTxs_InvalidID(t *testing.T) { } func TestSoloSequencer_SubmitBatchTxs_EmptyBatch(t *testing.T) { - seq := newTestSequencer(t, 0) + seq := newTestSequencer(t) res, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ Id: []byte("test"), @@ -100,26 +97,8 @@ func TestSoloSequencer_SubmitBatchTxs_EmptyBatch(t *testing.T) { assert.Empty(t, seq.queue) } -func TestSoloSequencer_SubmitBatchTxs_QueueFull(t *testing.T) { - seq := newTestSequencer(t, 3) // max 3 txs - - batch := coresequencer.Batch{Transactions: [][]byte{{1}, {2}, {3}}} - _, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ - Id: []byte("test"), - Batch: &batch, - }) - require.NoError(t, err) - - res, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ - Id: []byte("test"), - Batch: &coresequencer.Batch{Transactions: [][]byte{{4}}}, - }) - assert.ErrorIs(t, err, ErrQueueFull) - assert.Nil(t, res) -} - func TestSoloSequencer_GetNextBatch_EmptyQueue(t *testing.T) { - seq := newTestSequencer(t, 0) + seq := newTestSequencer(t) resp, err := seq.GetNextBatch(context.Background(), coresequencer.GetNextBatchRequest{Id: []byte("test")}) require.NoError(t, err) @@ -129,7 +108,7 @@ func TestSoloSequencer_GetNextBatch_EmptyQueue(t *testing.T) { } func TestSoloSequencer_GetNextBatch_InvalidID(t *testing.T) { - seq := newTestSequencer(t, 0) + seq := newTestSequencer(t) res, err := seq.GetNextBatch(context.Background(), coresequencer.GetNextBatchRequest{Id: []byte("wrong")}) assert.ErrorIs(t, err, ErrInvalidID) @@ -137,7 +116,7 @@ func TestSoloSequencer_GetNextBatch_InvalidID(t *testing.T) { } func TestSoloSequencer_GetNextBatch_DrainsAndFilters(t *testing.T) { - seq := newTestSequencer(t, 0) + seq := newTestSequencer(t) batch := coresequencer.Batch{Transactions: [][]byte{{1}, {2}, {3}}} _, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ @@ -175,8 +154,6 @@ func TestSoloSequencer_GetNextBatch_PostponedTxsRequeued(t *testing.T) { zerolog.Nop(), config.DefaultConfig(), []byte("test"), - 0, - genesis.Genesis{ChainID: "test"}, mockExec, ) @@ -197,7 +174,7 @@ func TestSoloSequencer_GetNextBatch_PostponedTxsRequeued(t *testing.T) { } func TestSoloSequencer_GetNextBatch_SubmitDuringProcessing(t *testing.T) { - seq := newTestSequencer(t, 0) + seq := newTestSequencer(t) batch := coresequencer.Batch{Transactions: [][]byte{{1}, {2}}} _, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ @@ -223,7 +200,7 @@ func TestSoloSequencer_GetNextBatch_SubmitDuringProcessing(t *testing.T) { } func TestSoloSequencer_VerifyBatch(t *testing.T) { - seq := newTestSequencer(t, 0) + seq := newTestSequencer(t) batchData := [][]byte{[]byte("batch1"), []byte("batch2")} @@ -236,35 +213,10 @@ func TestSoloSequencer_VerifyBatch(t *testing.T) { } func TestSoloSequencer_DAHeight(t *testing.T) { - seq := newTestSequencer(t, 0) + seq := newTestSequencer(t) assert.Equal(t, uint64(0), seq.GetDAHeight()) seq.SetDAHeight(42) assert.Equal(t, uint64(42), seq.GetDAHeight()) } - -func TestSoloSequencer_QueueFullThenFreed(t *testing.T) { - seq := newTestSequencer(t, 2) // max 2 txs - - _, err := seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ - Id: []byte("test"), - Batch: &coresequencer.Batch{Transactions: [][]byte{{1}, {2}}}, - }) - require.NoError(t, err) - - _, err = seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ - Id: []byte("test"), - Batch: &coresequencer.Batch{Transactions: [][]byte{{3}}}, - }) - assert.ErrorIs(t, err, ErrQueueFull) - - _, err = seq.GetNextBatch(context.Background(), coresequencer.GetNextBatchRequest{Id: []byte("test")}) - require.NoError(t, err) - - _, err = seq.SubmitBatchTxs(context.Background(), coresequencer.SubmitBatchTxsRequest{ - Id: []byte("test"), - Batch: &coresequencer.Batch{Transactions: [][]byte{{3}}}, - }) - assert.NoError(t, err, "submission should succeed after queue is drained") -} From 77b58a6bfb270ea5242a4dcc61586352c39bf10b Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Thu, 9 Apr 2026 12:28:03 +0200 Subject: [PATCH 4/9] updates --- CHANGELOG.md | 149 +++++++++--------- apps/testapp/cmd/run.go | 4 + block/internal/submitting/submitter.go | 1 + .../syncing/syncer_forced_inclusion_test.go | 4 +- pkg/sequencers/solo/README.md | 30 ++-- 5 files changed, 98 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b960ed27a..6513355c78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,47 +9,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changes + +- Make easier to override `DefaultMaxBlobSize` by ldflags [#3235](https://github.com/evstack/ev-node/pull/3235) +- Add solo sequencer (simple in memory single sequencer without force inclusion) [#3235](https://github.com/evstack/ev-node/pull/3235) + ## v1.1.0-rc.2 ### Changes -* Added publisher-mode synchronization option for failover scenarios with early P2P infrastructure readiness [#3222](https://github.com/evstack/ev-node/pull/3222) -* Improve P2P transient network failure [#3212](https://github.com/evstack/ev-node/pull/3212) -* Improve execution/evm check for stored meta not stale [#3221](https://github.com/evstack/ev-node/pull/3221) +- Added publisher-mode synchronization option for failover scenarios with early P2P infrastructure readiness [#3222](https://github.com/evstack/ev-node/pull/3222) +- Improve P2P transient network failure [#3212](https://github.com/evstack/ev-node/pull/3212) +- Improve execution/evm check for stored meta not stale [#3221](https://github.com/evstack/ev-node/pull/3221) ## v1.1.0-rc.1 ### Added -* Add AWS & GCP KMS signer backend [#3171](https://github.com/evstack/ev-node/pull/3171) -* Subscribe to forced inclusion namespace events [#3146](https://github.com/evstack/ev-node/pull/3146) -* Display block source in sync log [#3193](https://github.com/evstack/ev-node/pull/3193) +- Add AWS & GCP KMS signer backend [#3171](https://github.com/evstack/ev-node/pull/3171) +- Subscribe to forced inclusion namespace events [#3146](https://github.com/evstack/ev-node/pull/3146) +- Display block source in sync log [#3193](https://github.com/evstack/ev-node/pull/3193) ### Fixed -* Avoid evicting yet to be processed heights [#3204](https://github.com/evstack/ev-node/pull/3204) -* Bound Badger index cache memory to prevent growth with chain length [3209](https://github.com/evstack/ev-node/pull/3209) -* Refetch latest da height instead of da height +1 when P2P is offline [#3201](https://github.com/evstack/ev-node/pull/3201) -* Fix race on startup sync. [#3162](https://github.com/evstack/ev-node/pull/3162) -* Strict raft state. [#3167](https://github.com/evstack/ev-node/pull/3167) -* Retry fetching the timestamp on error in da-client [#3166](https://github.com/evstack/ev-node/pull/3166) +- Avoid evicting yet to be processed heights [#3204](https://github.com/evstack/ev-node/pull/3204) +- Bound Badger index cache memory to prevent growth with chain length [3209](https://github.com/evstack/ev-node/pull/3209) +- Refetch latest da height instead of da height +1 when P2P is offline [#3201](https://github.com/evstack/ev-node/pull/3201) +- Fix race on startup sync. [#3162](https://github.com/evstack/ev-node/pull/3162) +- Strict raft state. [#3167](https://github.com/evstack/ev-node/pull/3167) +- Retry fetching the timestamp on error in da-client [#3166](https://github.com/evstack/ev-node/pull/3166) ## v1.0.0 ### Fixed -* Persist cache snapshot only once at shutdown to avoid Badger vlog +- Persist cache snapshot only once at shutdown to avoid Badger vlog increase. [#3153](https://github.com/evstack/ev-node/pull/3153) ## v1.0.0-rc.5 ### Added -* Add disaster recovery for sequencer - * Catch up possible DA-only blocks when restarting. [#3057](https://github.com/evstack/ev-node/pull/3057) - * Verify DA and P2P state on restart (prevent double-signing). [#3061](https://github.com/evstack/ev-node/pull/3061) -* Node pruning support. [#2984](https://github.com/evstack/ev-node/pull/2984) - * Two different sort of pruning implemented: +- Add disaster recovery for sequencer + - Catch up possible DA-only blocks when restarting. [#3057](https://github.com/evstack/ev-node/pull/3057) + - Verify DA and P2P state on restart (prevent double-signing). [#3061](https://github.com/evstack/ev-node/pull/3061) +- Node pruning support. [#2984](https://github.com/evstack/ev-node/pull/2984) + - Two different sort of pruning implemented: _Classic pruning_ (`all`): prunes given `HEAD-n` blocks from the databases, including store metadatas. _Auto Storage Optimization_ (`metadata`): prunes only the state metadatas, keeps all blocks. By using one or the other, you are losing the ability to rollback or replay transactions earlier than `HEAD-n`. @@ -57,56 +62,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -* Fix block timer to account for execution time. Previously, the block timer reset to the full `block_time` duration +- Fix block timer to account for execution time. Previously, the block timer reset to the full `block_time` duration after `ProduceBlock` completed, making the effective interval `block_time + execution_time`. Now the timer subtracts elapsed execution time so blocks are produced at the configured cadence. ### Changes -* Store pending blocks separately from executed blocks key. [#3073](https://github.com/evstack/ev-node/pull/3073) -* Fixes issues with force inclusion verification on sync nodes. [#3057](https://github.com/evstack/ev-node/pull/3057) -* Add flag to `local-da` to produce empty DA blocks (closer to the real +- Store pending blocks separately from executed blocks key. [#3073](https://github.com/evstack/ev-node/pull/3073) +- Fixes issues with force inclusion verification on sync nodes. [#3057](https://github.com/evstack/ev-node/pull/3057) +- Add flag to `local-da` to produce empty DA blocks (closer to the real system). [#3057](https://github.com/evstack/ev-node/pull/3057) -* Validate P2P DA height hints against the latest known DA height to prevent malicious peers from triggering runaway +- Validate P2P DA height hints against the latest known DA height to prevent malicious peers from triggering runaway catchup. [#3128](https://github.com/evstack/ev-node/pull/3128) -* Replace syncer DA polling system by DA subscription via +- Replace syncer DA polling system by DA subscription via websockets. [#3131](https://github.com/evstack/ev-node/pull/3131) ## v1.0.0-rc.4 ### Changes -* Skip draining when exec client unavailable. [#3060](https://github.com/evstack/ev-node/pull/3060) +- Skip draining when exec client unavailable. [#3060](https://github.com/evstack/ev-node/pull/3060) ## v1.0.0-rc.3 ### Added -* Add DA Hints for P2P transactions. This allows a catching up node to be on sync with both DA and +- Add DA Hints for P2P transactions. This allows a catching up node to be on sync with both DA and P2P. ([#2891](https://github.com/evstack/ev-node/pull/2891)) ### Changes -* Improve `cache.NumPendingData` to not return empty data. Automatically bumps `LastSubmittedHeight` to reflect +- Improve `cache.NumPendingData` to not return empty data. Automatically bumps `LastSubmittedHeight` to reflect that. ([#3046](https://github.com/evstack/ev-node/pull/3046)) -* **BREAKING** Make pending events cache and tx cache fully ephemeral. Those will be re-fetched on restart. DA Inclusion +- **BREAKING** Make pending events cache and tx cache fully ephemeral. Those will be re-fetched on restart. DA Inclusion cache persists until cleared up after DA inclusion has been processed. Persist accross restart using store metadata. ([#3047](https://github.com/evstack/ev-node/pull/3047)) -* Replace LRU cache by standard mem cache with manual eviction in `store_adapter`. When P2P blocks were fetched too +- Replace LRU cache by standard mem cache with manual eviction in `store_adapter`. When P2P blocks were fetched too fast, they would be evicted before being executed [#3051](https://github.com/evstack/ev-node/pull/3051) -* Fix replay logic leading to app hashes by verifying against the wrong +- Fix replay logic leading to app hashes by verifying against the wrong block [#3053](https://github.com/evstack/ev-node/pull/3053). ## v1.0.0-rc.2 ### Changes -* Improve cache handling when there is a significant backlog of pending headers and +- Improve cache handling when there is a significant backlog of pending headers and data. ([#3030](https://github.com/evstack/ev-node/pull/3030)) -* Decrease MaxBytesSize to `5MB` to increase compatibility with public +- Decrease MaxBytesSize to `5MB` to increase compatibility with public nodes. ([#3030](https://github.com/evstack/ev-node/pull/3030)) -* Proper counting of `DASubmitterPendingBlobs` metrics. [#3038](https://github.com/evstack/ev-node/pull/3038) -* Replace `go-header` store by `ev-node` store. This avoid duplication of all blocks in `go-header` and `ev-node` store. +- Proper counting of `DASubmitterPendingBlobs` metrics. [#3038](https://github.com/evstack/ev-node/pull/3038) +- Replace `go-header` store by `ev-node` store. This avoid duplication of all blocks in `go-header` and `ev-node` store. Thanks to the cached store from #3030, this should improve p2p performance as well. [#3036](https://github.com/evstack/ev-node/pull/3036) @@ -114,45 +119,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* Added OpenTelemetry tracing support with OTLP export for distributed tracing across ev-node components including block +- Added OpenTelemetry tracing support with OTLP export for distributed tracing across ev-node components including block production, syncing, DA submission/retrieval, sequencer, store operations, and RPC layer. Configurable via `instrumentation.tracing`, `instrumentation.tracing_endpoint`, `instrumentation.tracing_service_name`, and `instrumentation.tracing_sample_rate` settings. ([#2956](https://github.com/evstack/ev-node/issues/2956)) -* **BREAKING:** Implement forced inclusion and batch sequencing ([#2797](https://github.com/evstack/ev-node/pull/2797)) +- **BREAKING:** Implement forced inclusion and batch sequencing ([#2797](https://github.com/evstack/ev-node/pull/2797)) **This change requires adding a `da_epoch_forced_inclusion` field to the node's `genesis.json` file.** The recommended value is `100`. Full support for this feature will be available in a future release. -* Added `post-tx` command and force inclusion server to submit transactions directly to the DA +- Added `post-tx` command and force inclusion server to submit transactions directly to the DA layer. ([#2888](https://github.com/evstack/ev-node/pull/2888)) Additionally, modified the core package to support marking transactions as forced included transactions. The execution client ought to perform basic validation on those transactions as they have skipped the execution client's mempool. -* Added batching strategies (default stay time-based, unchanged from previous betas). Currently available strategies are +- Added batching strategies (default stay time-based, unchanged from previous betas). Currently available strategies are `time`, `size`, `immediate` and `adaptive`. [Full documentation can be found here](https://github.com/evstack/ev-node/blob/122486de98d09ecd37d792b88814dcf07238f28a/docs/learn/config.md?plain=1#L521-L597). -* Added `FilterTxs` method to the execution interface. This method is meant to filter txs by size and if the execution +- Added `FilterTxs` method to the execution interface. This method is meant to filter txs by size and if the execution clients allows it, by gas. This is useful for force included transactions, as those aren't filtered by the sequencer's mempool. -* Added `GetExecutionInfo` method to the execution interface. This method returns some execution information, such as +- Added `GetExecutionInfo` method to the execution interface. This method returns some execution information, such as the maximum gas per block. ### Changed -* **BREAKING:** Renamed `evm-single` to `evm` and `grpc-single` to `evgrpc` for +- **BREAKING:** Renamed `evm-single` to `evm` and `grpc-single` to `evgrpc` for clarity. [#2839](https://github.com/evstack/ev-node/pull/2839). You may need to manually modify your evnode.yaml `signer.signer_path` if your $HOME folder is changed. -* Split cache interface into `CacheManager` and `PendingManager` and created `da` client to easy DA +- Split cache interface into `CacheManager` and `PendingManager` and created `da` client to easy DA handling. [#2878](https://github.com/evstack/ev-node/pull/2878) -* Improved startup DA retrieval height when cache is cleared or +- Improved startup DA retrieval height when cache is cleared or empty. [#2880](https://github.com/evstack/ev-node/pull/2880) ### Removed -* **BREAKING:** Removed unused and confusing metrics from sequencers and block processing, including sequencer-specific +- **BREAKING:** Removed unused and confusing metrics from sequencers and block processing, including sequencer-specific metrics (gas price, blob size, transaction status, pending blocks), channel buffer metrics, overly granular error metrics, block production categorization metrics, and sync lag metrics. Essential metrics for DA submission health, block production, and performance monitoring are retained. [#2904](https://github.com/evstack/ev-node/pull/2904) -* **BREAKING**: Removed `core/da` package and replaced DAClient with internal implementation. The DA client is exposed +- **BREAKING**: Removed `core/da` package and replaced DAClient with internal implementation. The DA client is exposed as `block.FullDAClient`, `block.DAClient`, `block.DAVerifier` without leaking implementation details. [#2910](https://github.com/evstack/ev-node/pull/2910) @@ -160,53 +165,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements -* Loosen syncer validation for allowing swapping sequencer and full node +- Loosen syncer validation for allowing swapping sequencer and full node state [#2925](https://github.com/evstack/ev-node/pull/2925) ## v1.0.0-beta.10 ### Added -* Enhanced health check system with separate liveness (`/health/live`) and readiness (`/health/ready`) HTTP endpoints. +- Enhanced health check system with separate liveness (`/health/live`) and readiness (`/health/ready`) HTTP endpoints. Readiness endpoint includes P2P listening check and aggregator block production rate validation (5x block time threshold). ([#2800](https://github.com/evstack/ev-node/pull/2800)) -* Added `GetP2PStoreInfo` RPC method to retrieve head/tail metadata for go-header stores used by P2P +- Added `GetP2PStoreInfo` RPC method to retrieve head/tail metadata for go-header stores used by P2P sync ([#2835](https://github.com/evstack/ev-node/pull/2835)) -* Added protobuf definitions for `P2PStoreEntry` and `P2PStoreSnapshot` messages to support P2P store inspection +- Added protobuf definitions for `P2PStoreEntry` and `P2PStoreSnapshot` messages to support P2P store inspection ### Changed -* Improved EVM execution client payload status validation with proper retry logic for SYNCING states in `InitChain`, +- Improved EVM execution client payload status validation with proper retry logic for SYNCING states in `InitChain`, `ExecuteTxs`, and `SetFinal` methods. The implementation now follows Engine API specification by retrying SYNCING/ACCEPTED status with exponential backoff and failing immediately on INVALID status, preventing unnecessary node shutdowns during transient execution engine sync operations. ([#2863](https://github.com/evstack/ev-node/pull/2863)) -* Remove GasPrice and GasMultiplier from DA interface and configuration to use celestia-node's native fee +- Remove GasPrice and GasMultiplier from DA interface and configuration to use celestia-node's native fee estimation. ([#2822](https://github.com/evstack/ev-node/pull/2822)) -* Use cache instead of in memory store for reaper. Persist cache on reload. Autoclean after 24 +- Use cache instead of in memory store for reaper. Persist cache on reload. Autoclean after 24 hours. ([#2811](https://github.com/evstack/ev-node/pull/2811)) -* Improved P2P sync service store initialization to be atomic and prevent race +- Improved P2P sync service store initialization to be atomic and prevent race conditions ([#2838](https://github.com/evstack/ev-node/pull/2838)) -* Enhanced P2P bootstrap behavior to intelligently detect starting height from local store instead of requiring trusted +- Enhanced P2P bootstrap behavior to intelligently detect starting height from local store instead of requiring trusted hash -* Relaxed execution layer height validation in block replay to allow execution to be ahead of target height, enabling +- Relaxed execution layer height validation in block replay to allow execution to be ahead of target height, enabling recovery from manual intervention scenarios ### Removed -* **BREAKING:** Removed `evnode.v1.HealthService` gRPC endpoint. Use HTTP endpoints: `GET /health/live` and +- **BREAKING:** Removed `evnode.v1.HealthService` gRPC endpoint. Use HTTP endpoints: `GET /health/live` and `GET /health/ready`. ([#2800](https://github.com/evstack/ev-node/pull/2800)) -* **BREAKING:** Removed `TrustedHash` configuration option and `--evnode.node.trusted_hash` flag. Sync service now +- **BREAKING:** Removed `TrustedHash` configuration option and `--evnode.node.trusted_hash` flag. Sync service now automatically determines starting height from local store state ([#2838](https://github.com/evstack/ev-node/pull/2838)) -* **BREAKING:** Removed unused and confusing metrics from sequencers and block processing, including sequencer-specific +- **BREAKING:** Removed unused and confusing metrics from sequencers and block processing, including sequencer-specific metrics (gas price, blob size, transaction status, pending blocks), channel buffer metrics, overly granular error metrics, block production categorization metrics, and sync lag metrics. Essential metrics for DA submission health, block production, and performance monitoring are retained. [#2904](https://github.com/evstack/ev-node/pull/2904) ### Fixed -* Fixed sync service initialization issue when node is not on genesis but has an empty store +- Fixed sync service initialization issue when node is not on genesis but has an empty store ## v1.0.0-beta.9 @@ -214,34 +219,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 -* Added automated upgrade test for the `evm` app that verifies compatibility when moving from v1.0.0-beta.8 to HEAD in +- Added automated upgrade test for the `evm` app that verifies compatibility when moving from v1.0.0-beta.8 to HEAD in CI ([#2780](https://github.com/evstack/ev-node/pull/2780)) -* Added execution-layer replay mechanism so nodes can resynchronize by replaying missed batches against the +- Added execution-layer replay mechanism so nodes can resynchronize by replaying missed batches against the executor ([#2771](https://github.com/evstack/ev-node/pull/2771)) -* Added cache-pruning logic that evicts entries once heights are finalized to keep node memory usage +- Added cache-pruning logic that evicts entries once heights are finalized to keep node memory usage bounded ([#2761](https://github.com/evstack/ev-node/pull/2761)) -* Added Prometheus gauges and counters that surface DA submission failures, pending blobs, and resend attempts for +- Added Prometheus gauges and counters that surface DA submission failures, pending blobs, and resend attempts for easier operational monitoring ([#2756](https://github.com/evstack/ev-node/pull/2756)) -* Added gRPC execution client implementation for remote execution services using Connect-RPC +- Added gRPC execution client implementation for remote execution services using Connect-RPC protocol ([#2490](https://github.com/evstack/ev-node/pull/2490)) -* Added `ExecutorService` protobuf definition with InitChain, GetTxs, ExecuteTxs, and SetFinal +- Added `ExecutorService` protobuf definition with InitChain, GetTxs, ExecuteTxs, and SetFinal RPCs ([#2490](https://github.com/evstack/ev-node/pull/2490)) -* Added new `grpc` app for running EVNode with a remote execution layer via +- Added new `grpc` app for running EVNode with a remote execution layer via gRPC ([#2490](https://github.com/evstack/ev-node/pull/2490)) ### Changed -* Hardened signer CLI and block pipeline per security audit: passphrases must be provided via +- Hardened signer CLI and block pipeline per security audit: passphrases must be provided via `--evnode.signer.passphrase_file`, JWT secrets must be provided via `--evm.jwt-secret-file`, data/header validation enforces metadata and timestamp checks, and the reaper backs off on failures ( BREAKING) ([#2764](https://github.com/evstack/ev-node/pull/2764)) -* Added retries around executor `ExecuteTxs` calls to better tolerate transient execution +- Added retries around executor `ExecuteTxs` calls to better tolerate transient execution errors ([#2784](https://github.com/evstack/ev-node/pull/2784)) -* Increased default `ReadinessMaxBlocksBehind` from 3 to 30 blocks so `/health/ready` stays true during normal batch +- Increased default `ReadinessMaxBlocksBehind` from 3 to 30 blocks so `/health/ready` stays true during normal batch sync ([#2779](https://github.com/evstack/ev-node/pull/2779)) -* Updated EVM execution client to use new `txpoolExt_getTxs` RPC API for retrieving pending transactions as RLP-encoded +- Updated EVM execution client to use new `txpoolExt_getTxs` RPC API for retrieving pending transactions as RLP-encoded bytes ### Deprecated @@ -252,7 +257,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 -* Removed `LastCommitHash`, `ConsensusHash`, and `LastResultsHash` from the canonical header representation in favor of +- Removed `LastCommitHash`, `ConsensusHash`, and `LastResultsHash` from the canonical header representation in favor of slim headers (BREAKING; legacy hashes now live under `Header.Legacy`) ([#2766](https://github.com/evstack/ev-node/pull/2766)) @@ -334,6 +339,6 @@ Pre-release versions: 0.x.y (anything may change) -* +- [Unreleased]: https://github.com/evstack/ev-node/compare/v1.0.0-beta.1...HEAD diff --git a/apps/testapp/cmd/run.go b/apps/testapp/cmd/run.go index 52b01d230f..a3c79b21ed 100644 --- a/apps/testapp/cmd/run.go +++ b/apps/testapp/cmd/run.go @@ -114,6 +114,10 @@ func createSequencer( executor execution.Executor, ) (coresequencer.Sequencer, error) { if enabled, _ := cmd.Flags().GetBool(flagSoloSequencer); enabled { + if nodeConfig.Node.BasedSequencer { + return nil, fmt.Errorf("solo sequencer cannot be used with based") + } + logger.Info().Msg("using solo sequencer") return solo.NewSoloSequencer(logger, nodeConfig, []byte(genesis.ChainID), executor), nil } diff --git a/block/internal/submitting/submitter.go b/block/internal/submitting/submitter.go index da181ec1cc..7a0ff721d1 100644 --- a/block/internal/submitting/submitter.go +++ b/block/internal/submitting/submitter.go @@ -260,6 +260,7 @@ func (s *Submitter) daSubmissionLoop() { return } + // Calculate total size (excluding signature) totalSize := uint64(0) for _, marshalled := range marshalledData { totalSize += uint64(len(marshalled)) diff --git a/block/internal/syncing/syncer_forced_inclusion_test.go b/block/internal/syncing/syncer_forced_inclusion_test.go index 7e43e55cc0..3c15fde125 100644 --- a/block/internal/syncing/syncer_forced_inclusion_test.go +++ b/block/internal/syncing/syncer_forced_inclusion_test.go @@ -480,7 +480,7 @@ func TestGracePeriodForEpoch_LightBlocks(t *testing.T) { func TestGracePeriodForEpoch_FullBlocks(t *testing.T) { s := &Syncer{daBlockBytes: make(map[uint64]uint64)} for h := uint64(0); h <= 4; h++ { - s.daBlockBytes[h] = uint64(common.DefaultMaxBlobSize) + s.daBlockBytes[h] = common.DefaultMaxBlobSize } grace := s.gracePeriodForEpoch(0, 4) require.GreaterOrEqual(t, grace, baseGracePeriodEpochs) @@ -501,7 +501,7 @@ func TestGracePeriodForEpoch_ExtendedUnderHighCongestion(t *testing.T) { // TestGracePeriodForEpoch_CappedAtMax verifies the grace period never exceeds maxGracePeriodEpochs. func TestGracePeriodForEpoch_CappedAtMax(t *testing.T) { s := &Syncer{daBlockBytes: make(map[uint64]uint64)} - huge := uint64(common.DefaultMaxBlobSize) * 100 + huge := common.DefaultMaxBlobSize * 100 for h := uint64(0); h <= 4; h++ { s.daBlockBytes[h] = huge } diff --git a/pkg/sequencers/solo/README.md b/pkg/sequencers/solo/README.md index 7f2670d51a..1e99213bb1 100644 --- a/pkg/sequencers/solo/README.md +++ b/pkg/sequencers/solo/README.md @@ -16,12 +16,12 @@ flowchart LR ## Design Decisions -| Decision | Rationale | -|---|---| -| In-memory queue | No persistence overhead; suitable for trusted single-operator setups | -| No forced inclusion | Avoids DA epoch tracking, checkpoint storage, and catch-up logic | -| No DA client dependency | `VerifyBatch` returns true unconditionally | -| Configurable queue limit | Provides backpressure when blocks can't be produced fast enough | +| Decision | Rationale | +| ------------------------ | -------------------------------------------------------------------- | +| In-memory queue | No persistence overhead; suitable for trusted single-operator setups | +| No forced inclusion | Avoids DA epoch tracking, checkpoint storage, and catch-up logic | +| No DA client dependency | `VerifyBatch` returns true unconditionally | +| Configurable queue limit | Provides backpressure when blocks can't be produced fast enough | ## Flow @@ -33,8 +33,6 @@ flowchart TD B -->|No| C["Return ErrInvalidID"] B -->|Yes| D{"Empty batch?"} D -->|Yes| E["Return OK"] - D -->|No| F{"Queue full?"} - F -->|Yes| G["Return ErrQueueFull"] F -->|No| H["Append txs to queue"] H --> E ``` @@ -80,11 +78,11 @@ resp, err := seq.GetNextBatch(ctx, coresequencer.GetNextBatchRequest{ ## Comparison with Other Sequencers -| Aspect | Solo | Single | Based | -|---|---|---|---| -| Mempool transactions | Yes | Yes | No | -| Forced inclusion | No | Yes | Yes | -| Persistence | None | DB-backed queue + checkpoints | Checkpoints only | -| Crash recovery | Lost on restart | Full recovery | Checkpoint-based | -| Catch-up mode | N/A | Yes | N/A | -| DA client required | No | Yes | Yes | +| Aspect | Solo | Single | Based | +| -------------------- | --------------- | ----------------------------- | ---------------- | +| Mempool transactions | Yes | Yes | No | +| Forced inclusion | No | Yes | Yes | +| Persistence | None | DB-backed queue + checkpoints | Checkpoints only | +| Crash recovery | Lost on restart | Full recovery | Checkpoint-based | +| Catch-up mode | N/A | Yes | N/A | +| DA client required | No | Yes | Yes | From c26c0909b0f17f6bdf1788ef856c253c88d55737 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Thu, 9 Apr 2026 14:59:01 +0200 Subject: [PATCH 5/9] fix ldflag --- apps/testapp/Dockerfile | 2 +- block/internal/common/consts.go | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/testapp/Dockerfile b/apps/testapp/Dockerfile index b3d9cdc021..ed4f2f2b73 100644 --- a/apps/testapp/Dockerfile +++ b/apps/testapp/Dockerfile @@ -28,7 +28,7 @@ COPY . . WORKDIR /ev-node/apps/testapp -RUN go build -ldflags "-X github.com/evstack/ev-node/block/internal/common.DefaultMaxBlobSize=125829120" -o /go/bin/testapp . +RUN go build -ldflags "-X github.com/evstack/ev-node/block/internal/common.defaultMaxBlobSizeStr=125829120" -o /go/bin/testapp . ## prep the final image. # diff --git a/block/internal/common/consts.go b/block/internal/common/consts.go index 857ee87690..0ac92a9655 100644 --- a/block/internal/common/consts.go +++ b/block/internal/common/consts.go @@ -1,6 +1,21 @@ package common -// DefaultMaxBlobSize is the fallback blob size limit used with the DA layer. +import "strconv" + +// defaultMaxBlobSizeStr holds the string representation of the default blob +// size limit. Override at link time via: // -// go build -ldflags "-X github.com/evstack/ev-node/block/internal/common.DefaultMaxBlobSize=10485760" -var DefaultMaxBlobSize uint64 = 5 * 1024 * 1024 // 5MB +// go build -ldflags "-X github.com/evstack/ev-node/block/internal/common.defaultMaxBlobSizeStr=125829120" +var defaultMaxBlobSizeStr = "5242880" // 5 MB + +// DefaultMaxBlobSize is the max blob size limit used for blob submission. +var DefaultMaxBlobSize uint64 = 5 * 1024 * 1024 + +func init() { + v, err := strconv.ParseUint(defaultMaxBlobSizeStr, 10, 64) + if err != nil || v == 0 { + DefaultMaxBlobSize = 5 * 1024 * 1024 + return + } + DefaultMaxBlobSize = v +} From a1c2f87e9f1054fc4be5cd9777b57977653ca4c7 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Thu, 9 Apr 2026 15:00:28 +0200 Subject: [PATCH 6/9] cleanup api --- pkg/sequencers/solo/README.md | 25 ------------------------- pkg/sequencers/solo/sequencer.go | 6 ++++-- pkg/sequencers/solo/sequencer_test.go | 3 --- 3 files changed, 4 insertions(+), 30 deletions(-) diff --git a/pkg/sequencers/solo/README.md b/pkg/sequencers/solo/README.md index 1e99213bb1..dc68241f41 100644 --- a/pkg/sequencers/solo/README.md +++ b/pkg/sequencers/solo/README.md @@ -51,31 +51,6 @@ flowchart TD H --> I["Return valid txs"] ``` -## Usage - -```go -seq := solo.NewSoloSequencer( - logger, - cfg, - []byte("chain-id"), - 1000, // maxQueueSize (0 = unlimited) - genesis, - executor, -) - -// Submit transactions from the mempool -seq.SubmitBatchTxs(ctx, coresequencer.SubmitBatchTxsRequest{ - Id: []byte("chain-id"), - Batch: &coresequencer.Batch{Transactions: txs}, -}) - -// Produce the next block -resp, err := seq.GetNextBatch(ctx, coresequencer.GetNextBatchRequest{ - Id: []byte("chain-id"), - MaxBytes: 500_000, -}) -``` - ## Comparison with Other Sequencers | Aspect | Solo | Single | Based | diff --git a/pkg/sequencers/solo/sequencer.go b/pkg/sequencers/solo/sequencer.go index 6f56e9ef8f..0fcae9f31c 100644 --- a/pkg/sequencers/solo/sequencer.go +++ b/pkg/sequencers/solo/sequencer.go @@ -12,7 +12,6 @@ import ( "github.com/evstack/ev-node/core/execution" coresequencer "github.com/evstack/ev-node/core/sequencer" - "github.com/evstack/ev-node/pkg/config" ) var ErrInvalidID = errors.New("invalid chain id") @@ -35,7 +34,6 @@ type SoloSequencer struct { func NewSoloSequencer( logger zerolog.Logger, - cfg config.Config, id []byte, executor execution.Executor, ) *SoloSequencer { @@ -128,6 +126,10 @@ func (s *SoloSequencer) GetNextBatch(ctx context.Context, req coresequencer.GetN } func (s *SoloSequencer) VerifyBatch(ctx context.Context, req coresequencer.VerifyBatchRequest) (*coresequencer.VerifyBatchResponse, error) { + if !s.isValid(req.Id) { + return nil, ErrInvalidID + } + return &coresequencer.VerifyBatchResponse{Status: true}, nil } diff --git a/pkg/sequencers/solo/sequencer_test.go b/pkg/sequencers/solo/sequencer_test.go index f7ae0e1518..7f3bc9e196 100644 --- a/pkg/sequencers/solo/sequencer_test.go +++ b/pkg/sequencers/solo/sequencer_test.go @@ -12,7 +12,6 @@ import ( "github.com/evstack/ev-node/core/execution" coresequencer "github.com/evstack/ev-node/core/sequencer" - "github.com/evstack/ev-node/pkg/config" "github.com/evstack/ev-node/test/mocks" ) @@ -42,7 +41,6 @@ func createDefaultMockExecutor(t *testing.T) *mocks.MockExecutor { func newTestSequencer(t *testing.T) *SoloSequencer { return NewSoloSequencer( zerolog.Nop(), - config.DefaultConfig(), []byte("test"), createDefaultMockExecutor(t), ) @@ -152,7 +150,6 @@ func TestSoloSequencer_GetNextBatch_PostponedTxsRequeued(t *testing.T) { seq := NewSoloSequencer( zerolog.Nop(), - config.DefaultConfig(), []byte("test"), mockExec, ) From 4d1b531493aba930c147c924ec6f818f9961b9e9 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Thu, 9 Apr 2026 15:01:11 +0200 Subject: [PATCH 7/9] better defaults --- apps/testapp/cmd/root.go | 2 +- apps/testapp/cmd/run.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/testapp/cmd/root.go b/apps/testapp/cmd/root.go index 17db405566..1a1108e2a0 100644 --- a/apps/testapp/cmd/root.go +++ b/apps/testapp/cmd/root.go @@ -22,7 +22,7 @@ func init() { // add more flags to RunCmd RunCmd.Flags().String(flagKVEndpoint, "", "Address and port for the KV executor HTTP server") - RunCmd.Flags().Bool(flagSoloSequencer, true, "Enable Solo sequencer (instead of based sequencer or single sequencer)") + RunCmd.Flags().Bool(flagSoloSequencer, false, "Enable Solo sequencer (instead of based sequencer or single sequencer)") } // RootCmd is the root command for Evolve diff --git a/apps/testapp/cmd/run.go b/apps/testapp/cmd/run.go index a3c79b21ed..75e5a49019 100644 --- a/apps/testapp/cmd/run.go +++ b/apps/testapp/cmd/run.go @@ -118,8 +118,7 @@ func createSequencer( return nil, fmt.Errorf("solo sequencer cannot be used with based") } - logger.Info().Msg("using solo sequencer") - return solo.NewSoloSequencer(logger, nodeConfig, []byte(genesis.ChainID), executor), nil + return solo.NewSoloSequencer(logger, []byte(genesis.ChainID), executor), nil } blobClient, err := blobrpc.NewWSClient(ctx, logger, nodeConfig.DA.Address, nodeConfig.DA.AuthToken, "") From 57cd3ef9c0d62d1659216dc8e92dcdc556b81913 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Thu, 9 Apr 2026 15:11:59 +0200 Subject: [PATCH 8/9] cleanup readme --- pkg/sequencers/solo/README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pkg/sequencers/solo/README.md b/pkg/sequencers/solo/README.md index dc68241f41..bdeb3cd7dc 100644 --- a/pkg/sequencers/solo/README.md +++ b/pkg/sequencers/solo/README.md @@ -16,12 +16,11 @@ flowchart LR ## Design Decisions -| Decision | Rationale | -| ------------------------ | -------------------------------------------------------------------- | -| In-memory queue | No persistence overhead; suitable for trusted single-operator setups | -| No forced inclusion | Avoids DA epoch tracking, checkpoint storage, and catch-up logic | -| No DA client dependency | `VerifyBatch` returns true unconditionally | -| Configurable queue limit | Provides backpressure when blocks can't be produced fast enough | +| Decision | Rationale | +| ----------------------- | -------------------------------------------------------------------- | +| In-memory queue | No persistence overhead; suitable for trusted single-operator setups | +| No forced inclusion | Avoids DA epoch tracking, checkpoint storage, and catch-up logic | +| No DA client dependency | `VerifyBatch` returns true unconditionally | ## Flow From 076f0943549b2875cef7b3de2bdb6e5c4a7cb088 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 10 Apr 2026 09:50:30 +0200 Subject: [PATCH 9/9] fixes --- apps/testapp/Dockerfile | 1 + block/internal/common/consts.go | 4 ++-- pkg/sequencers/solo/README.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/testapp/Dockerfile b/apps/testapp/Dockerfile index ed4f2f2b73..e41335f7b6 100644 --- a/apps/testapp/Dockerfile +++ b/apps/testapp/Dockerfile @@ -28,6 +28,7 @@ COPY . . WORKDIR /ev-node/apps/testapp +# 125829120 = 120 MB RUN go build -ldflags "-X github.com/evstack/ev-node/block/internal/common.defaultMaxBlobSizeStr=125829120" -o /go/bin/testapp . ## prep the final image. diff --git a/block/internal/common/consts.go b/block/internal/common/consts.go index 0ac92a9655..840b2faa97 100644 --- a/block/internal/common/consts.go +++ b/block/internal/common/consts.go @@ -9,12 +9,12 @@ import "strconv" var defaultMaxBlobSizeStr = "5242880" // 5 MB // DefaultMaxBlobSize is the max blob size limit used for blob submission. -var DefaultMaxBlobSize uint64 = 5 * 1024 * 1024 +var DefaultMaxBlobSize uint64 func init() { v, err := strconv.ParseUint(defaultMaxBlobSizeStr, 10, 64) if err != nil || v == 0 { - DefaultMaxBlobSize = 5 * 1024 * 1024 + DefaultMaxBlobSize = 5 * 1024 * 1024 // 5 MB fallback return } DefaultMaxBlobSize = v diff --git a/pkg/sequencers/solo/README.md b/pkg/sequencers/solo/README.md index bdeb3cd7dc..d58d33ed8a 100644 --- a/pkg/sequencers/solo/README.md +++ b/pkg/sequencers/solo/README.md @@ -32,7 +32,7 @@ flowchart TD B -->|No| C["Return ErrInvalidID"] B -->|Yes| D{"Empty batch?"} D -->|Yes| E["Return OK"] - F -->|No| H["Append txs to queue"] + D -->|No| H["Append txs to queue"] H --> E ```