From 5960890270012ba7eb90f163043767220d0978a8 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Thu, 16 Jul 2026 16:09:49 +0200 Subject: [PATCH 1/2] fix(evm): don't create single sequencer for non-aggregator nodes P2P-only followers (no DA address) crashed on startup with a nil pointer dereference: the EVM app's createSequencer unconditionally built a single sequencer, whose forced-inclusion retriever calls GetForcedInclusionNamespace on the nil DA client. Mirror the testapp guard from #3386 by skipping sequencer creation for non-aggregators, and fail fast in single.NewSequencer when the DA client is nil so future regressions surface as a clean error instead of a SIGSEGV. Co-Authored-By: Claude Fable 5 --- apps/evm/cmd/run.go | 4 ++++ pkg/sequencers/single/sequencer.go | 4 ++++ pkg/sequencers/single/sequencer_test.go | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/apps/evm/cmd/run.go b/apps/evm/cmd/run.go index ccb45b5a02..acbb2cd2af 100644 --- a/apps/evm/cmd/run.go +++ b/apps/evm/cmd/run.go @@ -160,6 +160,10 @@ func createSequencer( daClient block.FullDAClient, executor execution.Executor, ) (coresequencer.Sequencer, error) { + if !nodeConfig.Node.Aggregator { + return nil, nil + } + if nodeConfig.Node.BasedSequencer { // Based sequencer mode - fetch transactions only from DA if !nodeConfig.Node.Aggregator { diff --git a/pkg/sequencers/single/sequencer.go b/pkg/sequencers/single/sequencer.go index dd30e73f41..e7277ff651 100644 --- a/pkg/sequencers/single/sequencer.go +++ b/pkg/sequencers/single/sequencer.go @@ -78,6 +78,10 @@ func NewSequencer( genesis genesis.Genesis, executor execution.Executor, ) (*Sequencer, error) { + if daClient == nil { + return nil, errors.New("single sequencer requires a DA client") + } + s := &Sequencer{ db: db, logger: logger, diff --git a/pkg/sequencers/single/sequencer_test.go b/pkg/sequencers/single/sequencer_test.go index 1891cbba99..f4109b02c3 100644 --- a/pkg/sequencers/single/sequencer_test.go +++ b/pkg/sequencers/single/sequencer_test.go @@ -114,6 +114,13 @@ func newTestSequencer(t *testing.T, db ds.Batching, daClient block.FullDAClient) return seq } +func TestNewSequencer_NilDAClient(t *testing.T) { + mockExec := createDefaultMockExecutor(t) + seq, err := NewSequencer(zerolog.Nop(), ds.NewMapDatastore(), nil, config.DefaultConfig(), []byte("test"), 1000, genesis.Genesis{}, mockExec) + require.Error(t, err) + require.Nil(t, seq) +} + func TestSequencer_SubmitBatchTxs(t *testing.T) { dummyDA := newDummyDA(100_000_000) db := ds.NewMapDatastore() From 2f2bf76f24b7ef0b41e6fc6e9ccf7c8e58a9bd03 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Thu, 16 Jul 2026 16:14:55 +0200 Subject: [PATCH 2/2] fix: keep sequencer for promotable followers and require DA for promotable mode Promotable nodes start as followers but can be promoted to proposer at runtime, which hands the sequencer to the aggregator components. The non-aggregator guard (added to testapp in #3386 and mirrored here for the EVM app) would leave promotable nodes with a nil sequencer and break promotion. Create the sequencer for promotable nodes too, and require a DA address for promotable mode at config validation, same as aggregator mode. Co-Authored-By: Claude Fable 5 --- apps/evm/cmd/run.go | 4 +++- apps/testapp/cmd/run.go | 4 +++- pkg/cmd/run_node.go | 4 ++-- pkg/cmd/run_node_test.go | 22 ++++++++++++---------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/apps/evm/cmd/run.go b/apps/evm/cmd/run.go index acbb2cd2af..e573b2da7a 100644 --- a/apps/evm/cmd/run.go +++ b/apps/evm/cmd/run.go @@ -160,7 +160,9 @@ func createSequencer( daClient block.FullDAClient, executor execution.Executor, ) (coresequencer.Sequencer, error) { - if !nodeConfig.Node.Aggregator { + // Only aggregators and promotable nodes need a sequencer; promotable + // followers must have one ready before they are promoted to proposer. + if !nodeConfig.Node.Aggregator && !nodeConfig.Node.Promotable { return nil, nil } diff --git a/apps/testapp/cmd/run.go b/apps/testapp/cmd/run.go index 6eaaa33683..a89fc0f5b4 100644 --- a/apps/testapp/cmd/run.go +++ b/apps/testapp/cmd/run.go @@ -113,7 +113,9 @@ func createSequencer( genesis genesis.Genesis, executor execution.Executor, ) (coresequencer.Sequencer, error) { - if !nodeConfig.Node.Aggregator { + // Only aggregators and promotable nodes need a sequencer; promotable + // followers must have one ready before they are promoted to proposer. + if !nodeConfig.Node.Aggregator && !nodeConfig.Node.Promotable { return nil, nil } diff --git a/pkg/cmd/run_node.go b/pkg/cmd/run_node.go index fd98b0ee25..27dff229f0 100644 --- a/pkg/cmd/run_node.go +++ b/pkg/cmd/run_node.go @@ -48,8 +48,8 @@ func ParseStartConfig(cmd *cobra.Command) (rollconf.Config, error) { if err != nil { return rollconf.Config{}, err } - if nodeConfig.Node.Aggregator && nodeConfig.GetDAAddress() == "" { - return rollconf.Config{}, fmt.Errorf("DA address is required when aggregator mode is enabled") + if (nodeConfig.Node.Aggregator || nodeConfig.Node.Promotable) && nodeConfig.GetDAAddress() == "" { + return rollconf.Config{}, fmt.Errorf("DA address is required when aggregator or promotable mode is enabled") } return nodeConfig, nil diff --git a/pkg/cmd/run_node_test.go b/pkg/cmd/run_node_test.go index 6de41048fe..fef1300157 100644 --- a/pkg/cmd/run_node_test.go +++ b/pkg/cmd/run_node_test.go @@ -175,20 +175,22 @@ func TestAggregatorFlagInvariants(t *testing.T) { } func TestParseStartConfig_AggregatorRequiresDAAddress(t *testing.T) { - executor, sequencer, keyProvider, nodeKey, ds, stopDAHeightTicker := createTestComponents(context.Background(), t) - defer stopDAHeightTicker() + for _, flag := range []string{"--rollkit.node.aggregator", "--rollkit.node.promotable"} { + executor, sequencer, keyProvider, nodeKey, ds, stopDAHeightTicker := createTestComponents(context.Background(), t) + defer stopDAHeightTicker() - nodeConfig := rollconf.DefaultConfig() - nodeConfig.RootDir = t.TempDir() + nodeConfig := rollconf.DefaultConfig() + nodeConfig.RootDir = t.TempDir() - cmd := newRunNodeCmd(t.Context(), executor, sequencer, keyProvider, nodeKey, ds, nodeConfig) - require.NoError(t, cmd.ParseFlags([]string{"start", "--rollkit.node.aggregator"})) + cmd := newRunNodeCmd(t.Context(), executor, sequencer, keyProvider, nodeKey, ds, nodeConfig) + require.NoError(t, cmd.ParseFlags([]string{"start", flag})) - _, err := ParseConfig(cmd) - require.NoError(t, err) + _, err := ParseConfig(cmd) + require.NoError(t, err) - _, err = ParseStartConfig(cmd) - require.EqualError(t, err, "DA address is required when aggregator mode is enabled") + _, err = ParseStartConfig(cmd) + require.EqualError(t, err, "DA address is required when aggregator or promotable mode is enabled") + } } func TestPromotableFlagInvariants(t *testing.T) {