From 57ef68b9f3d62091da750e4151620155ff8fac9d Mon Sep 17 00:00:00 2001 From: bdchatham Date: Wed, 27 May 2026 12:35:26 -0700 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20v1=E2=86=92v2=20config=20schema=20m?= =?UTF-8?q?igration=20for=20seid=20v6.5=20write=20mode=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit seid v6.5 (sei-chain commit 0412e4e84) replaced the WriteMode enum to model the FlatKV migration lifecycle. cosmos_only → memiavl_only is now the breaking change that crashes any node provisioned with the old default. Changes: - types.go: add v2 WriteMode constants (memiavl_only, migrate_evm, evm_migrated, migrate_all_but_bank, all_migrated_but_bank, migrate_bank, flatkv_only, test_only_dual_write); update IsValid() to v2 values; keep v1 constants as Deprecated for migration reference - defaults.go: change StateCommit and StateStore WriteMode defaults from WriteModeCosmosOnly → WriteModeMemiavlOnly - config.go: bump CurrentVersion 1 → 2 - migrate.go: register v1→v2 migration (renames cosmos_only, dual_write, split_write in both StateCommit and StateStore WriteMode); add SeidVersionForSchema map so each schema version is traceable to its seid version boundary - config_test.go: update TestWriteMode_Validity for v2 semantics Fixes: https://github.com/sei-protocol/sei-config/issues/24 Co-Authored-By: Claude Sonnet 4.6 (1M context) --- config.go | 2 +- config_test.go | 7 +++++-- defaults.go | 4 ++-- migrate.go | 49 +++++++++++++++++++++++++++++++++---------------- types.go | 15 ++++++++++++++- 5 files changed, 55 insertions(+), 22 deletions(-) diff --git a/config.go b/config.go index 76a7bf9..19c8ea3 100644 --- a/config.go +++ b/config.go @@ -8,7 +8,7 @@ package seiconfig import "runtime" // CurrentVersion is the config schema version produced by this library. -const CurrentVersion = 1 +const CurrentVersion = 2 // DefaultSnapshotInterval is the default Tendermint state-sync snapshot // creation interval (in blocks) used when snapshot generation is enabled. diff --git a/config_test.go b/config_test.go index a9edaa9..37c9120 100644 --- a/config_test.go +++ b/config_test.go @@ -739,8 +739,11 @@ func TestNodeMode_IsFullnodeType(t *testing.T) { } func TestWriteMode_Validity(t *testing.T) { - if !WriteModeCosmosOnly.IsValid() { - t.Error("cosmos_only should be valid") + if !WriteModeMemiavlOnly.IsValid() { + t.Error("memiavl_only should be valid") + } + if WriteModeCosmosOnly.IsValid() { + t.Error("cosmos_only should not be valid in v2 (deprecated — use migration)") } if WriteMode("invalid").IsValid() { t.Error("'invalid' should not be valid") diff --git a/defaults.go b/defaults.go index 79c3f58..be98a8b 100644 --- a/defaults.go +++ b/defaults.go @@ -117,7 +117,7 @@ func baseDefaults() *SeiConfig { IAVLDisableFastNode: true, StateCommit: StateCommitConfig{ Enable: true, - WriteMode: WriteModeCosmosOnly, + WriteMode: WriteModeMemiavlOnly, ReadMode: ReadModeCosmosOnly, }, StateStore: StateStoreConfig{ @@ -128,7 +128,7 @@ func baseDefaults() *SeiConfig { PruneIntervalSeconds: 600, ImportNumWorkers: 1, KeepLastVersion: true, - WriteMode: WriteModeCosmosOnly, + WriteMode: WriteModeMemiavlOnly, ReadMode: ReadModeCosmosOnly, }, ReceiptStore: ReceiptStoreConfig{ diff --git a/migrate.go b/migrate.go index 038ec40..a7dba18 100644 --- a/migrate.go +++ b/migrate.go @@ -175,22 +175,39 @@ type AppliedMigration struct { // Default migration registry // --------------------------------------------------------------------------- -// DefaultMigrations returns the set of all known migrations for the sei-config -// schema. Currently empty since v1 is the initial version — migrations will be -// added here as the schema evolves. +// SeidVersionForSchema maps each config schema version to the minimum sei-chain +// (seid) version that introduced the breaking change requiring it. // -// Example of a future migration: -// -// Migration{ -// FromVersion: 1, -// ToVersion: 2, -// Description: "Rename evm.checktx_timeout to evm.check_tx_timeout", -// Migrate: func(cfg *SeiConfig) error { -// // Field was renamed; value is preserved by the struct. -// cfg.Version = 2 -// return nil -// }, -// } +// v1 → seid < v6.5 (cosmos_only write mode, legacy EVM routing) +// v2 → seid ≥ v6.5 (memiavl_only write mode, FlatKV migration scheme) +var SeidVersionForSchema = map[int]string{ + 1: "< v6.5", + 2: "≥ v6.5", +} + +// DefaultMigrations returns all known migrations for the sei-config schema. +// Each migration corresponds to a sei-chain version boundary; see SeidVersionForSchema. func DefaultMigrations() []Migration { - return []Migration{} + return []Migration{ + { + FromVersion: 1, + ToVersion: 2, + Description: "seid v6.5: rename WriteMode values to FlatKV migration scheme (cosmos_only→memiavl_only, dual_write→migrate_evm, split_write→evm_migrated)", + Migrate: func(cfg *SeiConfig) error { + rename := map[WriteMode]WriteMode{ + WriteModeCosmosOnly: WriteModeMemiavlOnly, + WriteModeDualWrite: WriteModeMigrateEVM, + WriteModeSplitWrite: WriteModeEVMMigrated, + } + if m, ok := rename[cfg.Storage.StateCommit.WriteMode]; ok { + cfg.Storage.StateCommit.WriteMode = m + } + if m, ok := rename[cfg.Storage.StateStore.WriteMode]; ok { + cfg.Storage.StateStore.WriteMode = m + } + cfg.Version = 2 + return nil + }, + }, + } } diff --git a/types.go b/types.go index 9ed20de..20c7e00 100644 --- a/types.go +++ b/types.go @@ -69,6 +69,17 @@ func Dur(d time.Duration) Duration { type WriteMode string const ( + // v2 write modes — FlatKV migration lifecycle (sei-chain ≥ v6.5). + WriteModeMemiavlOnly WriteMode = "memiavl_only" + WriteModeMigrateEVM WriteMode = "migrate_evm" + WriteModeEVMMigrated WriteMode = "evm_migrated" + WriteModeMigrateAllButBank WriteMode = "migrate_all_but_bank" + WriteModeAllMigratedButBank WriteMode = "all_migrated_but_bank" + WriteModeMigrateBank WriteMode = "migrate_bank" + WriteModeFlatKVOnly WriteMode = "flatkv_only" + WriteModeTestOnlyDualWrite WriteMode = "test_only_dual_write" + + // Deprecated: v1 write modes, accepted only during v1→v2 migration. WriteModeCosmosOnly WriteMode = "cosmos_only" WriteModeDualWrite WriteMode = "dual_write" WriteModeSplitWrite WriteMode = "split_write" @@ -77,7 +88,9 @@ const ( func (m WriteMode) IsValid() bool { switch m { - case WriteModeCosmosOnly, WriteModeDualWrite, WriteModeSplitWrite, WriteModeEVMOnly: + case WriteModeMemiavlOnly, WriteModeMigrateEVM, WriteModeEVMMigrated, + WriteModeMigrateAllButBank, WriteModeAllMigratedButBank, + WriteModeMigrateBank, WriteModeFlatKVOnly, WriteModeTestOnlyDualWrite: return true default: return false From a275da46d64fd7cd3cc3092a79267ec4f9a355a5 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Wed, 27 May 2026 12:38:56 -0700 Subject: [PATCH 2/3] =?UTF-8?q?test:=20add=20v1=E2=86=92v2=20migration=20r?= =?UTF-8?q?ound-trip=20and=20edge=20case=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 (1M context) --- migrate_test.go | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/migrate_test.go b/migrate_test.go index 748f146..f35e5e7 100644 --- a/migrate_test.go +++ b/migrate_test.go @@ -377,3 +377,96 @@ func TestDefaultMigrations_Valid(t *testing.T) { t.Fatalf("DefaultMigrations failed to register: %v", err) } } + +// v1ToV2Migration returns the v1→v2 migration from DefaultMigrations for tests +// that exercise the rename transform directly (bypassing post-migration +// validation, which rejects unknown/deprecated WriteMode values). +func v1ToV2Migration(t *testing.T) Migration { + t.Helper() + for _, m := range DefaultMigrations() { + if m.FromVersion == 1 && m.ToVersion == 2 { + return m + } + } + t.Fatal("DefaultMigrations missing v1→v2 migration") + return Migration{} +} + +// TestMigrateConfig_WriteModeRoundTrip runs the real v1→v2 migration through +// the registry pipeline (including post-migration validation) and asserts the +// deprecated cosmos_only write mode is renamed to memiavl_only in both stores. +func TestMigrateConfig_WriteModeRoundTrip(t *testing.T) { + r, err := NewMigrationRegistry(DefaultMigrations()...) + if err != nil { + t.Fatalf("NewMigrationRegistry: %v", err) + } + + cfg := DefaultForMode(ModeFull) + cfg.Version = 1 + cfg.Storage.StateCommit.WriteMode = WriteModeCosmosOnly + cfg.Storage.StateStore.WriteMode = WriteModeCosmosOnly + + result, err := r.MigrateConfig(cfg, 2) + if err != nil { + t.Fatalf("MigrateConfig: %v", err) + } + + if cfg.Version != 2 { + t.Errorf("version: got %d, want 2", cfg.Version) + } + if cfg.Storage.StateCommit.WriteMode != WriteModeMemiavlOnly { + t.Errorf("state_commit.write_mode: got %q, want %q", + cfg.Storage.StateCommit.WriteMode, WriteModeMemiavlOnly) + } + if cfg.Storage.StateStore.WriteMode != WriteModeMemiavlOnly { + t.Errorf("state_store.write_mode: got %q, want %q", + cfg.Storage.StateStore.WriteMode, WriteModeMemiavlOnly) + } + if len(result.Applied) != 1 { + t.Fatalf("applied migrations: got %d, want 1", len(result.Applied)) + } +} + +// TestV1ToV2_WriteModeRename covers every deprecated v1 write mode and asserts +// it maps to the expected v2 value. The unknown-value case asserts pass-through +// (the migration only renames known values; validation handles the rest). +func TestV1ToV2_WriteModeRename(t *testing.T) { + m := v1ToV2Migration(t) + + tests := []struct { + name string + in WriteMode + want WriteMode + }{ + {"cosmos_only renames to memiavl_only", WriteModeCosmosOnly, WriteModeMemiavlOnly}, + {"dual_write renames to migrate_evm", WriteModeDualWrite, WriteModeMigrateEVM}, + {"split_write renames to evm_migrated", WriteModeSplitWrite, WriteModeEVMMigrated}, + {"already-v2 memiavl_only is preserved", WriteModeMemiavlOnly, WriteModeMemiavlOnly}, + {"unknown value passes through unchanged", WriteMode("future_mode"), WriteMode("future_mode")}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := DefaultForMode(ModeFull) + cfg.Version = 1 + cfg.Storage.StateCommit.WriteMode = tc.in + cfg.Storage.StateStore.WriteMode = tc.in + + if err := m.Migrate(cfg); err != nil { + t.Fatalf("Migrate: %v", err) + } + + if cfg.Version != 2 { + t.Errorf("version: got %d, want 2", cfg.Version) + } + if cfg.Storage.StateCommit.WriteMode != tc.want { + t.Errorf("state_commit.write_mode: got %q, want %q", + cfg.Storage.StateCommit.WriteMode, tc.want) + } + if cfg.Storage.StateStore.WriteMode != tc.want { + t.Errorf("state_store.write_mode: got %q, want %q", + cfg.Storage.StateStore.WriteMode, tc.want) + } + }) + } +} From 8dc28b536bb3538e848a1c943c5b8f2978305e5d Mon Sep 17 00:00:00 2001 From: bdchatham Date: Wed, 27 May 2026 12:40:14 -0700 Subject: [PATCH 3/3] fix: address cross-review findings on v2 write mode migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - types.go: remove WriteModeEVMOnly — never a real deployed value, had no migration target (would fail IsValid() after migration) - migrate.go: delete SeidVersionForSchema exported var (YAGNI — no programmatic consumer); fold version mapping into doc comment - validate.go: add SeverityWarning for test_only_dual_write — valid per IsValid() to match seid's own parser, but explicitly flagged since sei-chain marks it "CRITICAL: never deploy to production" Co-Authored-By: Claude Sonnet 4.6 (1M context) --- migrate.go | 12 +++--------- types.go | 1 - validate.go | 3 +++ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/migrate.go b/migrate.go index a7dba18..6eaf335 100644 --- a/migrate.go +++ b/migrate.go @@ -175,18 +175,12 @@ type AppliedMigration struct { // Default migration registry // --------------------------------------------------------------------------- -// SeidVersionForSchema maps each config schema version to the minimum sei-chain -// (seid) version that introduced the breaking change requiring it. +// DefaultMigrations returns all known migrations for the sei-config schema. +// +// Schema version to seid version mapping: // // v1 → seid < v6.5 (cosmos_only write mode, legacy EVM routing) // v2 → seid ≥ v6.5 (memiavl_only write mode, FlatKV migration scheme) -var SeidVersionForSchema = map[int]string{ - 1: "< v6.5", - 2: "≥ v6.5", -} - -// DefaultMigrations returns all known migrations for the sei-config schema. -// Each migration corresponds to a sei-chain version boundary; see SeidVersionForSchema. func DefaultMigrations() []Migration { return []Migration{ { diff --git a/types.go b/types.go index 20c7e00..e46d099 100644 --- a/types.go +++ b/types.go @@ -83,7 +83,6 @@ const ( WriteModeCosmosOnly WriteMode = "cosmos_only" WriteModeDualWrite WriteMode = "dual_write" WriteModeSplitWrite WriteMode = "split_write" - WriteModeEVMOnly WriteMode = "evm_only" ) func (m WriteMode) IsValid() bool { diff --git a/validate.go b/validate.go index f23028a..1fb2426 100644 --- a/validate.go +++ b/validate.go @@ -260,6 +260,9 @@ func validateStorage(r *ValidationResult, cfg *SeiConfig) { if sc.WriteMode != "" && !sc.WriteMode.IsValid() { r.addError("storage.state_commit.write_mode", fmt.Sprintf("invalid write_mode: %q", sc.WriteMode)) } + if sc.WriteMode == WriteModeTestOnlyDualWrite { + r.addWarning("storage.state_commit.write_mode", "test_only_dual_write must not be used in production") + } if sc.ReadMode != "" && !sc.ReadMode.IsValid() { r.addError("storage.state_commit.read_mode", fmt.Sprintf("invalid read_mode: %q", sc.ReadMode)) }