From 2d0d158bbf7169146003256be06acc7c805836d4 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Wed, 15 Jul 2026 10:16:13 -0700 Subject: [PATCH 1/2] feat(state-store): default evm-ss-split=true for new nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds StateStoreConfig.EVMSSSplit (renders app.toml [state-store] evm-ss-split) and defaults it true, so newly generated node configs run the giga SS-store layout (EVM state in the SeiDB state-store). ss-enable/sc-enable already default true, so this is the one extra flag distinguishing a giga SS node from a plain one, per protocol guidance. Safety: - Additive optional field; no schema-version bump. Existing v2 configs load the zero value (false) and, via `,omitempty`, render no evm-ss-split key at all — their app.toml is byte-unchanged. Only newly generated configs carry it true. - Switching an existing node still requires a state-sync (enabling the split over an unsplit data dir fails seid's startup safety check); this change does not migrate existing configs. - Decoupled from the Giga parallel-execution engine (giga_executor.*), untouched. Round-trip + default render guarded by TestWriteConfigToDir_EVMSSSplit. Co-Authored-By: Claude Opus 4.8 (1M context) --- config.go | 1 + defaults.go | 1 + enrichments.go | 3 +++ io_write_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ legacy.go | 3 +++ 5 files changed, 54 insertions(+) diff --git a/config.go b/config.go index 19c8ea3..e13c62c 100644 --- a/config.go +++ b/config.go @@ -328,6 +328,7 @@ type MemIAVLConfig struct { type StateStoreConfig struct { Enable bool `toml:"enable"` + EVMSSSplit bool `toml:"evm_ss_split"` DBDirectory string `toml:"db_directory"` Backend string `toml:"backend"` AsyncWriteBuffer int `toml:"async_write_buffer"` diff --git a/defaults.go b/defaults.go index f9bc7bd..27f5e04 100644 --- a/defaults.go +++ b/defaults.go @@ -124,6 +124,7 @@ func baseDefaults() *SeiConfig { }, StateStore: StateStoreConfig{ Enable: true, + EVMSSSplit: true, Backend: BackendPebbleDB, AsyncWriteBuffer: 100, KeepRecent: 100_000, diff --git a/enrichments.go b/enrichments.go index 5ba6ed2..80dc631 100644 --- a/enrichments.go +++ b/enrichments.go @@ -223,6 +223,9 @@ func DefaultEnrichments() map[string][]FieldOption { "storage.state_store.enable": { WithDescription("Enable SeiDB state-store for historical queries."), }, + "storage.state_store.evm_ss_split": { + WithDescription("Store EVM state in the SeiDB state-store (renders app.toml [state-store] evm-ss-split). Defaulted on for new nodes; an existing node adopts it only via a state-sync, since enabling it over an unsplit data directory fails the startup safety check."), + }, "storage.state_store.backend": { WithDescription("State store backend: pebbledb, rocksdb."), }, diff --git a/io_write_test.go b/io_write_test.go index 9b0c4a8..4dcae6e 100644 --- a/io_write_test.go +++ b/io_write_test.go @@ -107,3 +107,49 @@ func TestWriteConfigToDir_ReceiptStoreUsesRSBackend(t *testing.T) { } } } + +// TestWriteConfigToDir_EVMSSSplit guards the giga state-store default: a new node +// renders app.toml [state-store] evm-ss-split=true (the one flag that distinguishes +// a giga SS node from a plain one), under the exact unprefixed key the binary reads +// — the sibling keys are ss-prefixed, but this one is bare evm-ss-split. A node left +// off (EVMSSSplit=false) omits the key entirely (omitempty), so an existing v2 +// node's rendered app.toml is unchanged. +func TestWriteConfigToDir_EVMSSSplit(t *testing.T) { + readApp := func(home string) string { + b, err := os.ReadFile(filepath.Join(home, configDir, appTomlFile)) + if err != nil { + t.Fatalf("ReadFile app.toml: %v", err) + } + return string(b) + } + + // Default: on, rendered as the exact bare key, and round-trips back to true. + home := t.TempDir() + if err := WriteConfigToDir(DefaultForMode(ModeFull), home); err != nil { + t.Fatalf("WriteConfigToDir: %v", err) + } + if app := readApp(home); !strings.Contains(app, "evm-ss-split = true") { + t.Errorf("default app.toml must render evm-ss-split = true") + } + if strings.Contains(readApp(home), "ss-evm-ss-split") { + t.Errorf("evm-ss-split must be the bare key, not ss-evm-ss-split — the binary reads it unprefixed") + } + got, err := ReadConfigFromDir(home) + if err != nil { + t.Fatalf("ReadConfigFromDir: %v", err) + } + if !got.Storage.StateStore.EVMSSSplit { + t.Errorf("round-trip: state_store.evm_ss_split got false, want true") + } + + // Off: omitempty drops the key, so an existing v2 node's app.toml is unchanged. + home2 := t.TempDir() + cfg := DefaultForMode(ModeFull) + cfg.Storage.StateStore.EVMSSSplit = false + if err := WriteConfigToDir(cfg, home2); err != nil { + t.Fatalf("WriteConfigToDir: %v", err) + } + if strings.Contains(readApp(home2), "evm-ss-split") { + t.Errorf("evm-ss-split=false must omit the key (omitempty), but it is present") + } +} diff --git a/legacy.go b/legacy.go index dc2f1da..a84006a 100644 --- a/legacy.go +++ b/legacy.go @@ -269,6 +269,7 @@ type legacyStateCommit struct { type legacyStateStore struct { Enable bool `toml:"ss-enable"` + EVMSSSplit bool `toml:"evm-ss-split,omitempty"` DBDirectory string `toml:"ss-db-directory"` Backend string `toml:"ss-backend"` AsyncWriteBuffer int `toml:"ss-async-write-buffer"` @@ -589,6 +590,7 @@ func (cfg *SeiConfig) toLegacyApp() legacyAppConfig { StateStore: legacyStateStore{ Enable: cfg.Storage.StateStore.Enable, + EVMSSSplit: cfg.Storage.StateStore.EVMSSSplit, DBDirectory: cfg.Storage.StateStore.DBDirectory, Backend: cfg.Storage.StateStore.Backend, AsyncWriteBuffer: cfg.Storage.StateStore.AsyncWriteBuffer, @@ -852,6 +854,7 @@ func fromLegacy(tm legacyTendermintConfig, app legacyAppConfig) *SeiConfig { }, StateStore: StateStoreConfig{ Enable: app.StateStore.Enable, + EVMSSSplit: app.StateStore.EVMSSSplit, DBDirectory: app.StateStore.DBDirectory, Backend: app.StateStore.Backend, AsyncWriteBuffer: app.StateStore.AsyncWriteBuffer, From 4b1dcaaf1cafc6d1e8dcb2d4495d59c0a4d98307 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Wed, 15 Jul 2026 10:59:25 -0700 Subject: [PATCH 2/2] fix(state-store): scope evm-ss-split default to full/RPC mode Addresses xreview: baseDefaults=true rendered a contradictory ss-enable=false + evm-ss-split=true on validator/seed. Per giga_store_migration.md the split is for RPC nodes only (validator/seed run no SS store; archive unsupported). So set the default in applyFullOverrides only, reset it in applyArchiveOverrides, and add a validateStateStore guard (evm_ss_split requires state_store.enable). Test broadened to a per-mode matrix; comments trimmed to house style. Co-Authored-By: Claude Opus 4.8 (1M context) --- defaults.go | 11 +++++++- enrichments.go | 2 +- io_write_test.go | 68 ++++++++++++++++++++++++------------------------ legacy.go | 6 ++++- validate.go | 4 +++ 5 files changed, 54 insertions(+), 37 deletions(-) diff --git a/defaults.go b/defaults.go index 27f5e04..e7b24af 100644 --- a/defaults.go +++ b/defaults.go @@ -124,7 +124,6 @@ func baseDefaults() *SeiConfig { }, StateStore: StateStoreConfig{ Enable: true, - EVMSSSplit: true, Backend: BackendPebbleDB, AsyncWriteBuffer: 100, KeepRecent: 100_000, @@ -297,6 +296,13 @@ func applyFullOverrides(cfg *SeiConfig) { cfg.API.GRPC.Enable = true cfg.API.GRPCWeb.Enable = true cfg.Storage.StateStore.Enable = true + // RPC/full nodes default to the split SS layout (EVM state in its own SS DB). + // Scoped here, not baseDefaults: only modes that run the SS store may set it, + // and per giga_store_migration.md the split is supported for RPC nodes only + // (validator/seed run no SS store; archive resets it below). A fresh node + // builds the split on bootstrap; an existing unsplit node adopts it only via a + // state-sync, so this default touches new nodes, not a migration path. + cfg.Storage.StateStore.EVMSSSplit = true cfg.Storage.StateStore.KeepRecent = 100_000 cfg.Storage.StateCommit.AsyncCommitBuffer = 100 cfg.Chain.MinRetainBlocks = 100_000 @@ -310,6 +316,9 @@ func applyArchiveOverrides(cfg *SeiConfig) { cfg.Storage.PruningStrategy = PruningNothing cfg.Storage.StateStore.KeepRecent = 0 + // Archive inherits full's overrides but the giga SS split is not supported for + // archive nodes yet (giga_store_migration.md: RPC nodes only), so reset it. + cfg.Storage.StateStore.EVMSSSplit = false // Only MinRetainBlocks disables receipt pruning at runtime; the next two // are emitted to document intent (see ReceiptStoreConfig). cfg.Chain.MinRetainBlocks = 0 diff --git a/enrichments.go b/enrichments.go index 80dc631..be23c08 100644 --- a/enrichments.go +++ b/enrichments.go @@ -224,7 +224,7 @@ func DefaultEnrichments() map[string][]FieldOption { WithDescription("Enable SeiDB state-store for historical queries."), }, "storage.state_store.evm_ss_split": { - WithDescription("Store EVM state in the SeiDB state-store (renders app.toml [state-store] evm-ss-split). Defaulted on for new nodes; an existing node adopts it only via a state-sync, since enabling it over an unsplit data directory fails the startup safety check."), + WithDescription("Store EVM state in the SeiDB state-store (app.toml [state-store] evm-ss-split)."), }, "storage.state_store.backend": { WithDescription("State store backend: pebbledb, rocksdb."), diff --git a/io_write_test.go b/io_write_test.go index 4dcae6e..277fe06 100644 --- a/io_write_test.go +++ b/io_write_test.go @@ -108,12 +108,12 @@ func TestWriteConfigToDir_ReceiptStoreUsesRSBackend(t *testing.T) { } } -// TestWriteConfigToDir_EVMSSSplit guards the giga state-store default: a new node -// renders app.toml [state-store] evm-ss-split=true (the one flag that distinguishes -// a giga SS node from a plain one), under the exact unprefixed key the binary reads -// — the sibling keys are ss-prefixed, but this one is bare evm-ss-split. A node left -// off (EVMSSSplit=false) omits the key entirely (omitempty), so an existing v2 -// node's rendered app.toml is unchanged. +// TestWriteConfigToDir_EVMSSSplit guards the giga state-store default per mode: only +// the full/RPC mode renders app.toml [state-store] evm-ss-split=true (the one flag +// distinguishing a giga SS node from a plain one). validator/seed run no SS store and +// archive is unsupported, so all three omit the key entirely (omitempty) — leaving +// their rendered app.toml unchanged. The key is bare evm-ss-split, not ss-prefixed +// like its siblings, because the binary reads it unprefixed. func TestWriteConfigToDir_EVMSSSplit(t *testing.T) { readApp := func(home string) string { b, err := os.ReadFile(filepath.Join(home, configDir, appTomlFile)) @@ -123,33 +123,33 @@ func TestWriteConfigToDir_EVMSSSplit(t *testing.T) { return string(b) } - // Default: on, rendered as the exact bare key, and round-trips back to true. - home := t.TempDir() - if err := WriteConfigToDir(DefaultForMode(ModeFull), home); err != nil { - t.Fatalf("WriteConfigToDir: %v", err) - } - if app := readApp(home); !strings.Contains(app, "evm-ss-split = true") { - t.Errorf("default app.toml must render evm-ss-split = true") - } - if strings.Contains(readApp(home), "ss-evm-ss-split") { - t.Errorf("evm-ss-split must be the bare key, not ss-evm-ss-split — the binary reads it unprefixed") - } - got, err := ReadConfigFromDir(home) - if err != nil { - t.Fatalf("ReadConfigFromDir: %v", err) - } - if !got.Storage.StateStore.EVMSSSplit { - t.Errorf("round-trip: state_store.evm_ss_split got false, want true") - } - - // Off: omitempty drops the key, so an existing v2 node's app.toml is unchanged. - home2 := t.TempDir() - cfg := DefaultForMode(ModeFull) - cfg.Storage.StateStore.EVMSSSplit = false - if err := WriteConfigToDir(cfg, home2); err != nil { - t.Fatalf("WriteConfigToDir: %v", err) - } - if strings.Contains(readApp(home2), "evm-ss-split") { - t.Errorf("evm-ss-split=false must omit the key (omitempty), but it is present") + // Mode matrix: full renders the split on; every other mode omits the key. + splitModes := map[NodeMode]bool{ModeFull: true} + for _, mode := range []NodeMode{ModeValidator, ModeSeed, ModeFull, ModeArchive} { + home := t.TempDir() + if err := WriteConfigToDir(DefaultForMode(mode), home); err != nil { + t.Fatalf("WriteConfigToDir(%s): %v", mode, err) + } + app := readApp(home) + switch { + case splitModes[mode]: + if !strings.Contains(app, "evm-ss-split = true") { + t.Errorf("%s: app.toml must render evm-ss-split = true", mode) + } + got, err := ReadConfigFromDir(home) + if err != nil { + t.Fatalf("ReadConfigFromDir(%s): %v", mode, err) + } + if !got.Storage.StateStore.EVMSSSplit { + t.Errorf("%s round-trip: state_store.evm_ss_split got false, want true", mode) + } + default: + if strings.Contains(app, "evm-ss-split") { + t.Errorf("%s: must omit evm-ss-split (mode runs no split SS store), but the key is present", mode) + } + } + if strings.Contains(app, "ss-evm-ss-split") { + t.Errorf("%s: evm-ss-split must be the bare key, not ss-evm-ss-split — the binary reads it unprefixed", mode) + } } } diff --git a/legacy.go b/legacy.go index a84006a..849402d 100644 --- a/legacy.go +++ b/legacy.go @@ -268,7 +268,11 @@ type legacyStateCommit struct { } type legacyStateStore struct { - Enable bool `toml:"ss-enable"` + Enable bool `toml:"ss-enable"` + // evm-ss-split is bare, NOT ss-prefixed — the binary reads it unprefixed + // under [state-store]. Do not add the ss- prefix (cf. rs-backend below). + // omitempty so a false value omits the key: an existing (v2) node's app.toml + // stays unchanged; only a node with the split on emits evm-ss-split = true. EVMSSSplit bool `toml:"evm-ss-split,omitempty"` DBDirectory string `toml:"ss-db-directory"` Backend string `toml:"ss-backend"` diff --git a/validate.go b/validate.go index 1fb2426..8ba1ad6 100644 --- a/validate.go +++ b/validate.go @@ -278,6 +278,10 @@ func validateStorage(r *ValidationResult, cfg *SeiConfig) { r.addWarning("storage.state_store.backend", fmt.Sprintf( "unusual backend %q; expected pebbledb or rocksdb", ss.Backend)) } + if ss.EVMSSSplit && !ss.Enable { + r.addError("storage.state_store.evm_ss_split", + "evm_ss_split requires state_store.enable=true (the split routes EVM state into the state store)") + } } func validateEVM(r *ValidationResult, cfg *SeiConfig) {