From 244e443312bf6e1457e8dedd06fd111fcf54c913 Mon Sep 17 00:00:00 2001 From: corey Date: Fri, 17 Jul 2026 10:43:42 +0800 Subject: [PATCH 1/2] fix(node): skip priv-validator file for layer1-verify validators PR #966 merged the validator path into the unified startup flow, making LoadOrGenFilePV run unconditionally. A from-scratch layer1-verify validator never starts the Tendermint consensus node, so it has no /data/ directory; LoadOrGenFilePV generates and Save()s a FilePV into that missing dir and panics (write-file-atomic: no such file or directory). Load the derivation config before the priv-validator step and only load the FilePV when the node will actually start Tendermint (i.e. not layer1). The executor is handed a nil pubkey in that case -- NewExecutor and the sequencer-set check both tolerate nil, and a layer1 validator is correctly never matched as a sequencer. Co-Authored-By: Claude Opus 4.8 (1M context) --- node/cmd/node/main.go | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/node/cmd/node/main.go b/node/cmd/node/main.go index ddf7f728c..e96af5341 100644 --- a/node/cmd/node/main.go +++ b/node/cmd/node/main.go @@ -15,6 +15,7 @@ import ( "github.com/morph-l2/go-ethereum/crypto" "github.com/morph-l2/go-ethereum/ethclient" "github.com/prometheus/client_golang/prometheus/promhttp" + tmcrypto "github.com/tendermint/tendermint/crypto" tmlog "github.com/tendermint/tendermint/libs/log" tmnode "github.com/tendermint/tendermint/node" "github.com/tendermint/tendermint/privval" @@ -131,8 +132,30 @@ func L2NodeMain(ctx *cli.Context) error { if err != nil { return err } - tmVal := privval.LoadOrGenFilePV(tmCfg.PrivValidatorKeyFile(), tmCfg.PrivValidatorStateFile()) - pubKey, _ := tmVal.GetPubKey() + + // Load derivation config here (not just before the switch below): its + // VerifyMode decides whether this node runs the Tendermint consensus node, + // which in turn decides whether we need a priv-validator file. + derivationCfg := derivation.DefaultConfig() + if err := derivationCfg.SetCliContext(ctx); err != nil { + return fmt.Errorf("derivation set cli context error: %v", err) + } + + // Only nodes that start the Tendermint consensus node need a priv-validator + // key/state file. Layer1-verify validators never start Tendermint (see the + // switch below); LoadOrGenFilePV would generate and Save a FilePV, forcing a + // /data/ directory that from-scratch validators never had and panicking + // when it is absent. Skip it and hand the executor a nil pubkey -- NewExecutor + // and the sequencer-set check both tolerate nil (a layer1 validator is never + // in the sequencer set, so its "am I a sequencer" check is correctly false). + var ( + tmVal *privval.FilePV + pubKey tmcrypto.PubKey + ) + if derivationCfg.VerifyMode != derivation.VerifyModeLayer1 { + tmVal = privval.LoadOrGenFilePV(tmCfg.PrivValidatorKeyFile(), tmCfg.PrivValidatorStateFile()) + pubKey, _ = tmVal.GetPubKey() + } // Reuse the shared syncer instance -- DevSequencer mode is the only path // that pulls a syncer out of NewExecutor, so we hand back the same one @@ -158,12 +181,6 @@ func L2NodeMain(ctx *cli.Context) error { return err } - // ========== Derivation config (loaded early to drive the layer1 branch below) ========== - derivationCfg := derivation.DefaultConfig() - if err := derivationCfg.SetCliContext(ctx); err != nil { - return fmt.Errorf("derivation set cli context error: %v", err) - } - switch { case isMockSequencer: ms, err = mock.NewSequencer(executor) From e8653e13739f345541ae12820ad4207c51a7c59f Mon Sep 17 00:00:00 2001 From: corey Date: Fri, 17 Jul 2026 10:43:42 +0800 Subject: [PATCH 2/2] fix(node): default layer1-verify derivation confirmations to finalized Before PR #966 the derivation default was rpc.FinalizedBlockNumber; the centralized-sequencer work changed it to a fixed latest-10 depth paired with the reorg detector. That is the right default for consensus fullnodes but changes long-standing validator behavior. Restore finalized for layer1-verify validators only: when VerifyMode is layer1 and the operator has not explicitly set --derivation.confirmations, default to finalized. Consensus fullnodes keep the latest-N default. Co-Authored-By: Claude Opus 4.8 (1M context) --- node/derivation/config.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/node/derivation/config.go b/node/derivation/config.go index 2f73092ce..29b6f9a39 100644 --- a/node/derivation/config.go +++ b/node/derivation/config.go @@ -170,6 +170,14 @@ func (c *Config) SetCliContext(ctx *cli.Context) error { } c.VerifyMode = normalized + // Layer1-verify validators historically derived only from finalized L1 data + // (the pre-centralized-sequencer default). Preserve that: unless the operator + // explicitly set --derivation.confirmations, a layer1 node reads finalized + // rather than the fixed-depth latest-N default that consensus fullnodes use. + if c.VerifyMode == VerifyModeLayer1 && !ctx.GlobalIsSet(flags.DerivationConfirmations.Name) { + c.L1.Confirmations = rpc.FinalizedBlockNumber + } + if ctx.GlobalIsSet(flags.DerivationReorgCheckDepth.Name) { c.ReorgCheckDepth = ctx.GlobalUint64(flags.DerivationReorgCheckDepth.Name) }