From 182e268893162188f48ba553c849cb969a27e025 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Thu, 7 May 2026 12:56:51 -0700 Subject: [PATCH 1/3] feat(evm): add EnabledLegacySeiApis to EVMConfig schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors sei-chain/evmrpc/config/config.go:143 (EnabledLegacySeiApis []string with toml tag enabled_legacy_sei_apis). This is the schema field that maps to [evm].enabled_legacy_sei_apis in app.toml — the allowlist that gates deprecated sei_* / sei2_* JSON-RPC methods. Without this field, ApplyOverrides rejects evm.enabled_legacy_sei_apis as "unknown override key" because BuildRegistry only sees fields that exist on SeiConfig. Combined with #14 (slice override support), this enables per-chain control of the legacy namespace via the SND spec.template.spec.overrides map. Default: []string{} (legacy namespace disabled). Matches the upstream sei-chain default and the production stance. Wired through: - config.go: field on EVMConfig - legacy.go: legacyEVM mapping + toLegacyApp + fromLegacy round-trip - defaults.go: explicit empty default in baseDefaults - registry_test: classification check (FieldTypeStringSlice) - config_test: round-trip assertion through Write/ReadConfigFromDir Co-Authored-By: Claude Opus 4.7 (1M context) --- config.go | 5 +++++ config_test.go | 5 +++++ defaults.go | 1 + legacy.go | 3 +++ registry_test.go | 1 + 5 files changed, 15 insertions(+) diff --git a/config.go b/config.go index a100318..08a0622 100644 --- a/config.go +++ b/config.go @@ -391,6 +391,11 @@ type EVMConfig struct { DenyList []string `toml:"deny_list"` + // EnabledLegacySeiApis lists deprecated sei_* / sei2_* JSON-RPC methods + // the EVM HTTP endpoint will serve. Empty disables the legacy namespace + // entirely. The namespace is scheduled for removal; use eth_* methods. + EnabledLegacySeiApis []string `toml:"enabled_legacy_sei_apis"` + MaxLogNoBlock int64 `toml:"max_log_no_block"` MaxBlocksForLog int64 `toml:"max_blocks_for_log"` MaxSubscriptionsNewHead uint64 `toml:"max_subscriptions_new_head"` diff --git a/config_test.go b/config_test.go index b499879..c33bcf5 100644 --- a/config_test.go +++ b/config_test.go @@ -227,6 +227,7 @@ func TestWriteReadRoundTrip(t *testing.T) { // so it does not round-trip through the legacy two-file format. original.Chain.Moniker = "test-node" original.EVM.HTTPPort = 9545 + original.EVM.EnabledLegacySeiApis = []string{"sei_getLogs", "sei_getBlockByNumber"} original.Storage.StateStore.KeepRecent = 50000 if err := WriteConfigToDir(original, dir); err != nil { @@ -253,6 +254,10 @@ func TestWriteReadRoundTrip(t *testing.T) { if loaded.EVM.HTTPPort != 9545 { t.Errorf("evm.http_port: got %d, want 9545", loaded.EVM.HTTPPort) } + if got := loaded.EVM.EnabledLegacySeiApis; len(got) != 2 || + got[0] != "sei_getLogs" || got[1] != "sei_getBlockByNumber" { + t.Errorf("evm.enabled_legacy_sei_apis: got %v, want [sei_getLogs sei_getBlockByNumber]", got) + } if loaded.Storage.StateStore.KeepRecent != 50000 { t.Errorf("state_store.keep_recent: got %d, want 50000", loaded.Storage.StateStore.KeepRecent) } diff --git a/defaults.go b/defaults.go index 2a7a256..da8f80a 100644 --- a/defaults.go +++ b/defaults.go @@ -161,6 +161,7 @@ func baseDefaults() *SeiConfig { CheckTxTimeout: Dur(5 * time.Second), MaxTxPoolTxs: 1000, DenyList: []string{}, + EnabledLegacySeiApis: []string{}, MaxLogNoBlock: 10_000, MaxBlocksForLog: 2000, MaxSubscriptionsNewHead: 10_000, diff --git a/legacy.go b/legacy.go index b8c248c..5753923 100644 --- a/legacy.go +++ b/legacy.go @@ -311,6 +311,7 @@ type legacyEVM struct { MaxTxPoolTxs uint64 `toml:"max_tx_pool_txs"` Slow bool `toml:"slow"` DenyList []string `toml:"deny_list"` + EnabledLegacySeiApis []string `toml:"enabled_legacy_sei_apis"` MaxLogNoBlock int64 `toml:"max_log_no_block"` MaxBlocksForLog int64 `toml:"max_blocks_for_log"` MaxSubscriptionsNewHead uint64 `toml:"max_subscriptions_new_head"` @@ -628,6 +629,7 @@ func (cfg *SeiConfig) toLegacyApp() legacyAppConfig { MaxTxPoolTxs: cfg.EVM.MaxTxPoolTxs, Slow: cfg.EVM.Slow, DenyList: cfg.EVM.DenyList, + EnabledLegacySeiApis: cfg.EVM.EnabledLegacySeiApis, MaxLogNoBlock: cfg.EVM.MaxLogNoBlock, MaxBlocksForLog: cfg.EVM.MaxBlocksForLog, MaxSubscriptionsNewHead: cfg.EVM.MaxSubscriptionsNewHead, @@ -895,6 +897,7 @@ func fromLegacy(tm legacyTendermintConfig, app legacyAppConfig) *SeiConfig { MaxTxPoolTxs: app.EVM.MaxTxPoolTxs, Slow: app.EVM.Slow, DenyList: app.EVM.DenyList, + EnabledLegacySeiApis: app.EVM.EnabledLegacySeiApis, MaxLogNoBlock: app.EVM.MaxLogNoBlock, MaxBlocksForLog: app.EVM.MaxBlocksForLog, MaxSubscriptionsNewHead: app.EVM.MaxSubscriptionsNewHead, diff --git a/registry_test.go b/registry_test.go index 0b9416b..589ab92 100644 --- a/registry_test.go +++ b/registry_test.go @@ -86,6 +86,7 @@ func TestBuildRegistry_FieldTypes(t *testing.T) { {"mempool.drop_priority_threshold", FieldTypeFloat}, {"network.rpc.timeout_broadcast_tx_commit", FieldTypeDuration}, {"tx_index.indexer", FieldTypeStringSlice}, + {"evm.enabled_legacy_sei_apis", FieldTypeStringSlice}, } for _, tt := range tests { From 05efc7a37da06ae2bf8403485e4008c8d535e60a Mon Sep 17 00:00:00 2001 From: bdchatham Date: Thu, 7 May 2026 13:01:59 -0700 Subject: [PATCH 2/3] refactor(evm): trim doc + add enrichment + env-var test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address coral platform-engineer review of #15: - Trim the EnabledLegacySeiApis doc comment to match surrounding fields (DenyList, MaxTxPoolTxs, etc. are uncommented). The toml tag plus enrichment description carry the meaning. - Add enrichment entry adjacent to evm.deny_list in DefaultEnrichments. Deliberately omits WithHotReload — the legacy-API allowlist is registered when the EVM RPC mux is built at startup, not on config reload, so claiming hot-reload would mislead operators. - Add TestResolveEnv_StringSlice asserting that SEI_EVM_ENABLED_LEGACY_SEI_APIS=a,b is parsed correctly via the env resolution path. Locks the implicit env-var contract that adding the field to the registry creates. Co-Authored-By: Claude Opus 4.7 (1M context) --- config.go | 3 --- config_test.go | 11 +++++++++++ enrichments.go | 3 +++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/config.go b/config.go index 08a0622..76a7bf9 100644 --- a/config.go +++ b/config.go @@ -391,9 +391,6 @@ type EVMConfig struct { DenyList []string `toml:"deny_list"` - // EnabledLegacySeiApis lists deprecated sei_* / sei2_* JSON-RPC methods - // the EVM HTTP endpoint will serve. Empty disables the legacy namespace - // entirely. The namespace is scheduled for removal; use eth_* methods. EnabledLegacySeiApis []string `toml:"enabled_legacy_sei_apis"` MaxLogNoBlock int64 `toml:"max_log_no_block"` diff --git a/config_test.go b/config_test.go index c33bcf5..1303fe6 100644 --- a/config_test.go +++ b/config_test.go @@ -609,6 +609,17 @@ func TestResolveEnv_LegacyPrefix(t *testing.T) { } } +func TestResolveEnv_StringSlice(t *testing.T) { + cfg := Default() + t.Setenv("SEI_EVM_ENABLED_LEGACY_SEI_APIS", "sei_getLogs,sei_getBlockByNumber") + + ResolveEnv(cfg) + got := cfg.EVM.EnabledLegacySeiApis + if len(got) != 2 || got[0] != "sei_getLogs" || got[1] != "sei_getBlockByNumber" { + t.Errorf("after ResolveEnv: got %v, want [sei_getLogs sei_getBlockByNumber]", got) + } +} + func TestResolveEnv_SEIPrecedence(t *testing.T) { cfg := Default() t.Setenv("SEI_CHAIN_MIN_GAS_PRICES", "0.5usei") diff --git a/enrichments.go b/enrichments.go index 7ddebc9..5ba6ed2 100644 --- a/enrichments.go +++ b/enrichments.go @@ -295,6 +295,9 @@ func DefaultEnrichments() map[string][]FieldOption { WithDescription("RPC methods that should immediately fail (e.g. debug_traceBlockByNumber)."), WithHotReload(), }, + "evm.enabled_legacy_sei_apis": { + WithDescription("Deprecated sei_*/sei2_* JSON-RPC methods served on the EVM HTTP endpoint."), + }, "evm.max_log_no_block": { WithDescription("Maximum logs returned when block range is open-ended."), }, From af1c805d7cd60e52b0fe9311c084f93e757064f8 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Thu, 7 May 2026 13:04:14 -0700 Subject: [PATCH 3/3] test(evm): lock nil-vs-empty TOML asymmetry for EnabledLegacySeiApis TestStringSliceMissingKeyDecodesAsNil exercises the third asymmetry case the existing round-trip tests don't cover: a SeiConfig with EnabledLegacySeiApis = nil writes app.toml without the key, and re-reading produces nil (not the []string{} default). Combined with TestApplyOverrides_StringSliceRoundTripTOML, the three write/read paths are now locked: nil -> key omitted -> reads as nil []string{} -> "field = []" -> reads as []string{} ["a","b"] -> "field = [..]" -> reads as ["a","b"] Downstream consumers branching on nil vs non-nil-empty (e.g. "was this configured at all?") depend on this asymmetry. A future BurntSushi/toml change that normalized the encoding would silently break that branch; this test would catch it at config-package CI. Co-Authored-By: Claude Opus 4.7 (1M context) --- config_test.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/config_test.go b/config_test.go index 1303fe6..1efa196 100644 --- a/config_test.go +++ b/config_test.go @@ -535,6 +535,43 @@ func TestApplyOverrides_StringSliceOverwritesDefault(t *testing.T) { } } +// TestStringSliceMissingKeyDecodesAsNil locks the asymmetry between +// "field absent in app.toml" and "field present as empty list": +// +// nil -> key omitted from written TOML -> reads back as nil +// []string{} -> "field = []" in written TOML -> reads back as []string{} +// +// Downstream consumers branching on nil vs non-nil-empty (e.g. "was this +// configured at all?") rely on this. BurntSushi/toml v1.5.0 honors the +// distinction; a future encoder change that normalized nil and empty +// would silently break that branch. +func TestStringSliceMissingKeyDecodesAsNil(t *testing.T) { + cfg := DefaultForMode(ModeFull) + cfg.EVM.EnabledLegacySeiApis = nil + + dir := t.TempDir() + if err := WriteConfigToDir(cfg, dir); err != nil { + t.Fatalf("WriteConfigToDir: %v", err) + } + + appToml, err := os.ReadFile(filepath.Join(dir, "config", "app.toml")) + if err != nil { + t.Fatalf("read app.toml: %v", err) + } + if strings.Contains(string(appToml), "enabled_legacy_sei_apis") { + t.Errorf("nil slice should be omitted from app.toml; got it written") + } + + loaded, err := ReadConfigFromDir(dir) + if err != nil { + t.Fatalf("ReadConfigFromDir: %v", err) + } + if loaded.EVM.EnabledLegacySeiApis != nil { + t.Errorf("missing key should decode as nil; got %v (len %d)", + loaded.EVM.EnabledLegacySeiApis, len(loaded.EVM.EnabledLegacySeiApis)) + } +} + func TestApplyOverrides_StringSliceRoundTripTOML(t *testing.T) { dir := t.TempDir()