Skip to content

Add gov proposal based migration trigger#3650

Open
yzang2019 wants to merge 10 commits into
mainfrom
yzang/add-migration-trigger
Open

Add gov proposal based migration trigger#3650
yzang2019 wants to merge 10 commits into
mainfrom
yzang/add-migration-trigger

Conversation

@yzang2019

@yzang2019 yzang2019 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

The state-commitment (SC) store's in-flight memiavl → flatkv migration was previously paced by a node-local config (sc-keys-to-migrate-per-block). Because migration writes feed the AppHash, a per-node rate is consensus-relevant and a divergence risk: two validators draining at different rates fork the chain.
This PR makes the per-block migration rate a module-agnostic governance parameter (migration.NumKeysToMigratePerBlock) that every validator reads from chain state each BeginBlock and applies identically. The gov param also serves as the migration trigger: it defaults to 0 (paused), and raising it via a param-change proposal starts the drain fleet-wide at a deterministic height.
The old node-local config and its fallback are removed entirely — the gov param is now the sole source of the rate.

Key changes

New generic governance parameter

  • app/migration/params.go — new module-agnostic migration params subspace defining NumKeysToMigratePerBlock (default 0), its KeyTable, and validation. Deliberately not EVM-specific so future module migrations can reuse it.
  • app/app.go — registers the migration subspace; stores the *rootmulti.Store on the app and fails fast if the (unsupported) legacy commit multistore is in use.
  • app/abci.goBeginBlock reads NumKeysToMigratePerBlock from chain state and pushes it into the SC store before the block's first write (applyMigrationBatchSize).

Plumbing: push the rate down to the migration router

  • sei-db/state_db/sc/types/types.goCommitter interface gains SetMigrationBatchSize(int) error.
  • sei-cosmos/storev2/rootmulti/store.go — forwards SetMigrationBatchSize to the SC store; adds GetMigrationBatchSize for observability/tests.
  • sei-db/state_db/sc/composite/store.go — holds the gov-set batch size (atomic.Int64); buildRouter and SetMigrationBatchSize use it directly. Removed the effectiveMigrationBatchSize config fallback0 means paused, full stop.
  • sei-db/state_db/sc/migration/*Router interface gains SetMigrationBatchSize; all router types implement it. Only the migration manager acts on it; every other router (module/passthrough/dual-write/thread-safe) treats it as a no-op. router_builder.go now allows a 0 batch size (paused) instead of rejecting it.

Removed the node-local config + fallback

  • sei-db/config/sc_config.go, sei-db/config/toml.go, docker/localnode/config/app.toml, app/seidb.go — dropped the sc-keys-to-migrate-per-block field, default, validation, TOML entry, and flag parsing.

Tests

  • app/migration/params_test.go — unit tests for the new param (default, key-table registration, validation).
  • app/abci_test.go — subspace registration, applyMigrationBatchSize (defaults/set/clamp), and cross-block "param set in block N takes effect in N+1" coverage.
  • SC/rootmulti unit tests now seed the rate via SetMigrationBatchSize(...) after construction instead of the removed config field. The random-migration framework re-applies the rate on every store (re)open, faithfully mirroring how production re-pushes the gov param after each restart.

Integration test

  • integration_test/gov_module/ — new ParameterChangeProposal test that raises NumKeysToMigratePerBlock and asserts it takes effect.
  • integration_test/contracts/verify_flatkv_evm_migrate.sh — rewritten to drive the migration via a governance param-change proposal (submit → deposit → quorum vote → poll for PASSED → verify on-chain) instead of injecting the removed config.

Docs

  • AGENTS.md — Code style now mandates running both gofmt and goimports on every touched .go file.

@cursor

cursor Bot commented Jun 26, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes consensus-visible state migration pacing and AppHash determinism; mis-governance or timing bugs could fork validators or stall/resume migration fleet-wide.

Overview
Replaces node-local migration pacing with a consensus governance parameter so every validator drains memiavl→flatkv at the same per-block rate (migration writes affect AppHash).

Adds a generic migration x/params subspace with NumKeysToMigratePerBlock (default 0 = paused). Each BeginBlock reads it from chain state (lazy-seeding the default for gov proposals), clamps to int64, and pushes it into the storev2 rootmulti / composite SC layer via new SetMigrationBatchSize / GetMigrationBatchSize plumbing through routers and MigrationManager (batch 0 is valid and pauses boundary advances).

Removes sc-keys-to-migrate-per-block from StateCommit config, TOML, flags, and validation. Tests and the FlatKV migrate integration script now raise the rate through a ParameterChangeProposal instead of app.toml; AGENTS.md now requires goimports alongside gofmt on touched Go files.

Reviewed by Cursor Bugbot for commit a5cf528. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedJun 26, 2026, 5:50 PM

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.72973% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.69%. Comparing base (0ff5a52) to head (a5cf528).

Files with missing lines Patch % Lines
app/abci.go 69.23% 2 Missing and 2 partials ⚠️
app/app.go 57.14% 2 Missing and 1 partial ⚠️
sei-cosmos/storev2/rootmulti/store.go 62.50% 2 Missing and 1 partial ⚠️
sei-db/state_db/sc/memiavl/store.go 0.00% 2 Missing ⚠️
sei-db/state_db/sc/migration/module_router.go 77.77% 1 Missing and 1 partial ⚠️
sei-db/state_db/sc/migration/dual_write_router.go 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3650      +/-   ##
==========================================
- Coverage   59.16%   58.69%   -0.47%     
==========================================
  Files        2262     2214      -48     
  Lines      187009   182068    -4941     
==========================================
- Hits       110643   106867    -3776     
+ Misses      66430    65680     -750     
+ Partials     9936     9521     -415     
Flag Coverage Δ
sei-chain-pr 59.54% <72.22%> (?)
sei-db 70.41% <ø> (ø)
sei-db-state-db ?
sei-db-state-db-pr 77.72% <86.84%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
app/migration/params.go 100.00% <100.00%> (ø)
app/seidb.go 77.46% <ø> (+3.14%) ⬆️
sei-db/config/sc_config.go 100.00% <ø> (+12.50%) ⬆️
sei-db/state_db/sc/composite/store.go 72.39% <100.00%> (+0.41%) ⬆️
sei-db/state_db/sc/migration/migration_manager.go 95.43% <100.00%> (+0.10%) ⬆️
sei-db/state_db/sc/migration/migration_types.go 80.00% <ø> (ø)
sei-db/state_db/sc/migration/passthrough_router.go 100.00% <100.00%> (ø)
sei-db/state_db/sc/migration/router_builder.go 56.84% <ø> (+1.14%) ⬆️
sei-db/state_db/sc/migration/thread_safe_router.go 100.00% <100.00%> (ø)
sei-db/state_db/sc/migration/dual_write_router.go 96.55% <0.00%> (-3.45%) ⬇️
... and 5 more

... and 91 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread app/app.go Outdated
// composite SC backend drives the in-flight memiavl->flatkv migration that
// BeginBlock paces via the migration gov param. Fail fast if the legacy
// root multistore is somehow in use.
rs, ok := app.CommitMultiStore().(*rootmulti.Store)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need to cast like this, would it make sense to change the type returned by app.CommitMultiStore()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense

Comment thread app/app.go Outdated
genesisImportConfig genesistypes.GenesisImportConfig

stateStore seidb.StateStore
rs *rootmulti.Store

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, would it make sense to use a more descriptive name like rootStore? Out of context, it's not obvious what rs stands for.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good suggestion, will change

Comment on lines +105 to +106
if migrationBatchSize < 0 {
return nil, fmt.Errorf("migration batch size must not be negative, got %d", migrationBatchSize)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make migration batch size an unsigned integer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's a good point

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried that, looks like it would require a big refactory, will actually do a fallback here to 0 if it's negative

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A well-structured, thoroughly-tested change that moves the SC migration rate from a node-local config to a consensus-deterministic governance param (migration.NumKeysToMigratePerBlock), correctly read once per block in BeginBlock and pushed down to the composite SC store. No blocking correctness/security issues found; a few non-blocking notes, chiefly that the new subspace value is not genesis-exportable.

Findings: 0 blocking | 6 non-blocking | 2 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Migration param is not genesis-exportable (confirms Codex P2). x/params ExportGenesis (sei-cosmos/x/params/keeper/genesis.go) only exports FeesParams/CosmosGasParams, and the new migration subspace has no owning module's InitGenesis/ExportGenesis. After a seid export/import while a migration is in flight, NumKeysToMigratePerBlock is dropped and BeginBlock re-seeds the 0 (paused) default, halting the drain until governance re-submits a proposal. Data isn't lost (the boundary cursor lives in flatkv) and it's recoverable, but worth either adding genesis plumbing for this subspace or documenting the operational caveat.
  • Operational behavior change worth calling out in release notes: because the param defaults to 0 (paused) and is the sole source of the rate, any chain already running in a migrate write-mode will pause its drain at the upgrade height until a param-change proposal raises the rate. Intentional per the consensus-safety design, but operators must know to submit the proposal.
  • REVIEW_GUIDELINES.md and cursor-review.md were empty/absent, so no repo-specific guidelines or Cursor second opinion were available; only the Codex pass (one P2, addressed above) contributed.
  • Integration test nit (integration_test/gov_module/gov_proposal_test.yaml): the verifier NEW_PARAM == 12345 compares an unquoted numeric against a value the pipeline produces as a string (jq -r .value | tr -d "\""), unlike the sibling assertion NEW_ABCI_PARAM == "true". If the eval engine is type-strict this could mis-compare; quoting "12345" would match the established pattern.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread app/app.go Outdated
// composite SC backend drives the in-flight memiavl->flatkv migration that
// BeginBlock paces via the migration gov param. Fail fast if the legacy
// root multistore is somehow in use.
rs, ok := app.CommitMultiStore().(*rootmulti.Store)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This unconditional type assertion now panics in New() if the commit multistore is not *rootmulti.Store. New() is invoked by every seid subcommand that builds the app (export, query tooling, etc.), so please confirm no supported configuration (e.g. a legacy IAVL/storev2-disabled path) can reach here — otherwise this turns a previously-degraded path into a hard crash. If the legacy store is truly unsupported this is fine; a one-line note on why it's unreachable would help.

Comment thread app/abci.go
}
numKeys := migration.DefaultNumKeysToMigratePerBlock
if subspace, ok := app.ParamsKeeper.GetSubspace(migration.SubspaceName); ok {
// The migration subspace has no owning module to seed it in InitGenesis,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The lazy seed correctly handles a fresh/never-set param deterministically, but because this subspace has no module ExportGenesis (x/params only exports Fees/CosmosGas params), the value is lost across a seid export/import: a mid-migration rate resets to the 0 default here and the drain pauses until governance re-raises it. Consider adding genesis import/export plumbing for this subspace, or documenting the caveat.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 91e1766. Configure here.

Comment thread app/receipt_store_config.go Outdated
Comment thread sei-db/state_db/sc/migration/migration_manager.go Outdated
seidroid[bot]
seidroid Bot previously requested changes Jun 26, 2026

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR cleanly moves the SC migration rate from a node-local config to a consensus-safe governance param, with thorough plumbing and tests. However, an accidental rename of the receipt-store backend constant breaks an existing test (and decouples the test harness from the real config key), which blocks merge.

Findings: 1 blocking | 4 non-blocking | 2 posted inline

Blockers

  • None at the file/PR level.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • Cursor's second-opinion pass produced no output (cursor-review.md is empty); REVIEW_GUIDELINES.md is also empty, so no repo-specific standards could be applied.
  • validateNumKeysToMigratePerBlock accepts any uint64. Because the value is cast to int before NextBatch (migration_manager.go:299), a governance value above math.MaxInt64 becomes negative and halts commit deterministically. Consider bounding the param to a sane operational maximum.
  • app/abci_test.go's comment that math.MaxUint64 is 'forwarded verbatim with no conversion or clamping' is misleading — the app layer stores it verbatim, but migration_manager converts it to int downstream.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread app/receipt_store_config.go Outdated

const (
receiptStoreBackendKey = "receipt-store.rs-backend"
receiptStoreBackendKey = "receipt-store.rootStore-backend"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocker] This key must stay receipt-store.rs-backend. The parser in sei-db/config/receipt_config.go reads flagRSBackend = "receipt-store.rs-backend" (and the rs-backend mapstructure tag), so renaming this constant to receipt-store.rootStore-backend decouples the app-layer key from what ReadReceiptConfig actually reads. Concretely, TestParseReceiptConfigs_RejectsInvalidBackend (app/seidb_test.go) now sets "rocksdb" under a key the parser ignores, the config falls back to the default pebbledb, and assert.Error fails — breaking CI. This looks like collateral damage from a global rsrootStore rename (the comment at app/test_helpers.go:486 was hit too). Revert to receipt-store.rs-backend.

if advanceMigration {
// Get the next batch of keys to migrate.
valuesToMigrate, newBoundary, err := m.iterator.NextBatch(m.migrationBatchSize)
valuesToMigrate, newBoundary, err := m.iterator.NextBatch(int(m.migrationBatchSize))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] int(m.migrationBatchSize) truncates a uint64 gov param. On 64-bit, a value above math.MaxInt64 becomes negative, NextBatch rejects size <= 0, and ApplyChangeSets returns an error during commit — a deterministic chain halt from a governance typo. Since validateNumKeysToMigratePerBlock accepts any uint64, consider bounding the param to a sane maximum (well under math.MaxInt) at validation time.

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converts the SC migration per-block rate from a consensus-unsafe node-local config into a deterministic governance param read in BeginBlock; the design is sound, AppHash-safe (all nodes read the same value, lazy-default-set runs identically), and very thoroughly tested at unit, store, and integration levels. Only minor, non-blocking observations.

Findings: 0 blocking | 5 non-blocking | 1 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • New() now hard-panics if the commit multistore is not *rootmulti.Store (app/app.go:560). This is intentional fail-fast, but it is a behavior change: any App-construction path that could use a non-storev2 store (offline/CLI commands, tooling) would now crash rather than degrade. Worth confirming every code path that builds App goes through the storev2 rootmulti store.
  • The two later casts in app.go (~line 770 and ~2685) still re-derive the store via app.CommitMultiStore().(*rootmulti.Store) instead of reusing the new app.rootStore field; minor redundancy now that the type is guaranteed at construction.
  • Cursor's second-opinion review file (cursor-review.md) was empty — that pass produced no output. Codex reported no material issues.
  • Consider a brief note/log when the lazy default-set writes the param the first time (app/abci.go), purely for operator visibility on the upgrade block; not required for correctness.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread app/app.go
@seidroid seidroid Bot dismissed their stale review June 26, 2026 17:55

Superseded: latest AI review found no blocking issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants