From c94583452249e1596427d147ac034c207568b6a3 Mon Sep 17 00:00:00 2001 From: alexander-sei Date: Mon, 14 Sep 2026 12:47:17 +0200 Subject: [PATCH] dev(mock_block_validation): node-local SC migration rate override Adds SEI_UNSAFE_MIGRATION_BATCH_SIZE, read only by mock_block_validation builds, which replaces the on-chain NumKeysToMigratePerBlock rate in applyMigrationBatchSize. Production builds compile a no-op and are unchanged. A node-local rate makes the node's migration writes, and therefore its AppHash, differ from the network's from the first migration commit. The mock_block_validation consensus policy swallows that mismatch, which is the only build where such an override can exist. This is what lets a single node walk memiavl_only -> migrate_evm -> ... -> flatkv_only against a chain whose gov param is still 0, for testing FlatKV-only and ingress bootstrapping against live network state. The value is parsed at process start so a malformed override fails every subcommand rather than leaving the migration silently paused. --- app/abci.go | 11 ++-- app/unsafe_migration_batch_size.go | 9 ++++ app/unsafe_migration_batch_size_mock.go | 53 ++++++++++++++++++++ app/unsafe_migration_batch_size_mock_test.go | 37 ++++++++++++++ app/unsafe_migration_batch_size_test.go | 15 ++++++ 5 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 app/unsafe_migration_batch_size.go create mode 100644 app/unsafe_migration_batch_size_mock.go create mode 100644 app/unsafe_migration_batch_size_mock_test.go create mode 100644 app/unsafe_migration_batch_size_test.go diff --git a/app/abci.go b/app/abci.go index a7ee43fc09..40ceb17890 100644 --- a/app/abci.go +++ b/app/abci.go @@ -57,11 +57,9 @@ func (app *App) BeginBlock( } } -// applyMigrationBatchSize paces the SC store's background data migration at the network-agreed rate. -// The NumKeysToMigratePerBlock gov param is read from chain state so every node -// applies the same value each block; a per-node rate would diverge the -// AppHash. 0 (the default until a gov proposal raises it) leaves the migration -// paused; it is the sole source of the rate (there is no node-local fallback). +// applyMigrationBatchSize paces the SC store's background data migration at the +// rate the NumKeysToMigratePerBlock gov param holds in chain state, so every +// node applies the same value each block. 0 leaves the migration paused. func (app *App) applyMigrationBatchSize(ctx sdk.Context) { if app.rootStore == nil { return @@ -78,6 +76,9 @@ func (app *App) applyMigrationBatchSize(ctx sdk.Context) { } subspace.GetIfExists(ctx, migration.KeyNumKeysToMigratePerBlock, &numKeys) } + // A per-node rate diverges the AppHash, so production builds return numKeys + // unchanged here; only mock_block_validation builds may substitute one. + numKeys = unsafeMigrationBatchSizeOverride(numKeys) // Defense-in-depth: gov validation already rejects values above // MaxNumKeysToMigratePerBlock, but clamp here too so an out-of-range value // reaching state via any path can never overflow the int cast or trigger an diff --git a/app/unsafe_migration_batch_size.go b/app/unsafe_migration_batch_size.go new file mode 100644 index 0000000000..a90fe618cf --- /dev/null +++ b/app/unsafe_migration_batch_size.go @@ -0,0 +1,9 @@ +//go:build !mock_block_validation + +package app + +// unsafeMigrationBatchSizeOverride returns numKeys unchanged. Only a +// mock_block_validation build carries a node-local migration rate. +func unsafeMigrationBatchSizeOverride(numKeys uint64) uint64 { + return numKeys +} diff --git a/app/unsafe_migration_batch_size_mock.go b/app/unsafe_migration_batch_size_mock.go new file mode 100644 index 0000000000..841505e73f --- /dev/null +++ b/app/unsafe_migration_batch_size_mock.go @@ -0,0 +1,53 @@ +//go:build mock_block_validation + +package app + +import ( + "fmt" + "os" + "strconv" + "sync" +) + +// UnsafeMigrationBatchSizeEnv names the environment variable that replaces the +// on-chain NumKeysToMigratePerBlock rate with a node-local one. Only +// mock_block_validation builds read it. +const UnsafeMigrationBatchSizeEnv = "SEI_UNSAFE_MIGRATION_BATCH_SIZE" + +var unsafeMigrationBatchSize, unsafeMigrationBatchSizeSet = readUnsafeMigrationBatchSize( + os.Getenv(UnsafeMigrationBatchSizeEnv)) + +var logUnsafeMigrationBatchSizeOnce sync.Once + +// readUnsafeMigrationBatchSize parses the override value. An empty value means +// unset; any other value must be a base-10 unsigned integer. +func readUnsafeMigrationBatchSize(raw string) (uint64, bool) { + if raw == "" { + return 0, false + } + n, err := strconv.ParseUint(raw, 10, 64) + if err != nil { + // Parsed at process start so a typo fails every subcommand loudly rather + // than leaving a migration silently paused at the on-chain rate of 0. + panic(fmt.Sprintf("%s=%q is not a base-10 unsigned integer: %v", UnsafeMigrationBatchSizeEnv, raw, err)) + } + return n, true +} + +// unsafeMigrationBatchSizeOverride returns the node-local migration rate when +// SEI_UNSAFE_MIGRATION_BATCH_SIZE is set, and numKeys otherwise. +func unsafeMigrationBatchSizeOverride(numKeys uint64) uint64 { + if !unsafeMigrationBatchSizeSet { + return numKeys + } + logUnsafeMigrationBatchSizeOnce.Do(func() { + // A node-local rate makes this node's migration writes, and so its + // AppHash, differ from the network's from the first migration commit. + // The mock_block_validation consensus policy swallows that mismatch, + // which is the only reason this override can exist in this build. + logger.Error("UNSAFE: SC migration batch size overridden by environment; this node's AppHash "+ + "diverges from the network once migration starts", + "env", UnsafeMigrationBatchSizeEnv, "batchSize", unsafeMigrationBatchSize, "onChain", numKeys) + }) + return unsafeMigrationBatchSize +} diff --git a/app/unsafe_migration_batch_size_mock_test.go b/app/unsafe_migration_batch_size_mock_test.go new file mode 100644 index 0000000000..ef4d740892 --- /dev/null +++ b/app/unsafe_migration_batch_size_mock_test.go @@ -0,0 +1,37 @@ +//go:build mock_block_validation + +package app + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestReadUnsafeMigrationBatchSize(t *testing.T) { + v, ok := readUnsafeMigrationBatchSize("") + require.False(t, ok) + require.Zero(t, v) + + v, ok = readUnsafeMigrationBatchSize("250000") + require.True(t, ok) + require.Equal(t, uint64(250000), v) + + v, ok = readUnsafeMigrationBatchSize("0") + require.True(t, ok) + require.Zero(t, v) + + require.Panics(t, func() { readUnsafeMigrationBatchSize("lots") }) + require.Panics(t, func() { readUnsafeMigrationBatchSize("-1") }) +} + +func TestUnsafeMigrationBatchSizeOverride_FollowsProcessEnv(t *testing.T) { + // The override is fixed at process start, so assert against whatever this + // test process was started with rather than mutating the environment. + got := unsafeMigrationBatchSizeOverride(7) + if unsafeMigrationBatchSizeSet { + require.Equal(t, unsafeMigrationBatchSize, got) + return + } + require.Equal(t, uint64(7), got) +} diff --git a/app/unsafe_migration_batch_size_test.go b/app/unsafe_migration_batch_size_test.go new file mode 100644 index 0000000000..f13045c44e --- /dev/null +++ b/app/unsafe_migration_batch_size_test.go @@ -0,0 +1,15 @@ +//go:build !mock_block_validation + +package app + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUnsafeMigrationBatchSizeOverride_ProductionBuildIgnoresEnv(t *testing.T) { + t.Setenv("SEI_UNSAFE_MIGRATION_BATCH_SIZE", "12345") + require.Equal(t, uint64(7), unsafeMigrationBatchSizeOverride(7)) + require.Equal(t, uint64(0), unsafeMigrationBatchSizeOverride(0)) +}