Skip to content

Add gov proposal based migration trigger#3650

Merged
yzang2019 merged 26 commits into
mainfrom
yzang/add-migration-trigger
Jul 2, 2026
Merged

Add gov proposal based migration trigger#3650
yzang2019 merged 26 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
Migration batch size and write-mode transitions directly affect AppHash and validator agreement; misconfiguration or partial kick-off would fork or halt the chain.

Overview
Consensus-controlled migration pacing replaces the removed sc-keys-to-migrate-per-block app.toml knob. A new migration params subspace exposes NumKeysToMigratePerBlock (default 0 = paused, max 1M); BeginBlock reads it and calls rootStore.SetMigrationBatchSize, with lazy default seeding, clamping, and log-on-error (no panic).

The SC stack gains SetMigrationBatchSize end-to-end (composite router / MigrationManager, including 0 = paused). On auto stores, a positive batch size in BeginBlock can kick off memiavl_only → migrate_evm; pinned memiavl_only nodes skip and opt out. SetWriteMode no longer rebuilds cached KV views so mid-block kick-off does not orphan the deliver cache-multi-store.

sc-write-mode-enable-auto (default true) makes effective write mode auto unless explicitly disabled; legacy configs with only memiavl_only still join gov-driven migration. Docker/integration tests pin modes where needed and verify_flatkv_evm_migrate.sh drives the rate via a param-change proposal instead of local config.

AGENTS.md now requires goimports alongside gofmt on touched Go files.

Reviewed by Cursor Bugbot for commit 16c93d0. 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✅ passedJul 2, 2026, 1:06 AM

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.51515% with 31 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.79%. Comparing base (1c66d87) to head (16c93d0).

Files with missing lines Patch % Lines
sei-cosmos/storev2/rootmulti/store.go 65.78% 10 Missing and 3 partials ⚠️
app/abci.go 69.23% 2 Missing and 2 partials ⚠️
sei-db/state_db/sc/composite/store.go 71.42% 4 Missing ⚠️
app/app.go 57.14% 2 Missing and 1 partial ⚠️
app/seidb.go 33.33% 1 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.29%   58.79%   -0.50%     
==========================================
  Files        2272     2223      -49     
  Lines      188210   183096    -5114     
==========================================
- Hits       111594   107647    -3947     
+ Misses      66565    65839     -726     
+ Partials    10051     9610     -441     
Flag Coverage Δ
sei-chain-pr 64.66% <75.55%> (+8.02%) ⬆️
sei-db 70.41% <ø> (ø)
sei-db-state-db ?
sei-db-state-db-pr 77.03% <78.57%> (?)

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%> (ø)
sei-cosmos/server/config/config.go 92.00% <100.00%> (+0.16%) ⬆️
sei-db/config/sc_config.go 100.00% <100.00%> (+11.76%) ⬆️
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-ibc-go/testing/simapp/test_helpers.go 52.15% <100.00%> (+0.20%) ⬆️
sei-wasmd/app/test_helpers.go 44.10% <100.00%> (+0.21%) ⬆️
... and 8 more

... and 89 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 Outdated
@seidroid seidroid Bot dismissed their stale review June 26, 2026 17:55

Superseded: latest AI review found no blocking issues.

seidroid[bot]
seidroid Bot previously requested changes Jun 29, 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 plumbing of the per-block migration rate into a governance param is clean, well-documented, and well-tested, but the PR also flips the default WriteMode to migrate_evm and relies on the claim that "paused migrate_evm produces the same app hash as memiavl_only" — which the routing code contradicts: brand-new EVM keys are written to flatkv yet excluded from the AppHash while paused, creating a fork/integrity risk. REVIEW_GUIDELINES.md and cursor-review.md were both empty; only Codex provided a second opinion, and its two P1 findings are corroborated.

Findings: 5 blocking | 4 non-blocking | 3 posted inline

Blockers

  • Default WriteMode + migration trigger are not consistently network-wide. The default flips to migrate_evm (sc_config.go) but sc-write-mode remains node-local (app/seidb.go:109 only overrides when the toml key is non-empty). A node that picks up the new default runs migrate_evm while a peer with an explicit sc-write-mode = memiavl_only stays memiavl_only; once the gov param is raised, only the migrate_evm nodes drain. Because the paused-migrate_evm AppHash is NOT equivalent to memiavl_only (see inline finding), this mixed fleet can fork even before the migration is triggered. The PR's framing of the gov param as the 'sole' migration trigger overstates this — the write mode is still a consensus-relevant local switch. (Corroborates Codex P1 #2.)
  • Missing test coverage for the central safety claim: there is no test asserting that a paused migrate_evm store and a memiavl_only store produce identical AppHashes when brand-new EVM keys are written each block. Existing migration tests all set a positive batch size and drive the migration; the steady-state/paused path with new-key writes (the exact rollout state introduced by the default change) is unverified.
  • 3 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • REVIEW_GUIDELINES.md is empty/missing, so no repo-specific standards were applied. cursor-review.md is empty — the Cursor pass produced no output. Only the Codex pass produced findings (both corroborated here).
  • app/app.go: the fail-fast panic message says expected *storev2_rootmulti.Store but the import alias was changed to rootmulti in this PR — cosmetic mismatch only.
  • integration_test/gov_module/gov_proposal_test.yaml: the verifier NEW_PARAM == 12345 compares a string (from jq -r .value | tr -d '"') against an integer literal, whereas the sibling test uses a quoted string (NEW_ABCI_PARAM == "true"). Confirm the eval framework coerces types here, otherwise the assertion may not behave as intended. There is also a stray q gov proposal ... status query inserted between the two node votes — harmless but reads oddly.
  • app/abci.go applyMigrationBatchSize lazily persists the default param via subspace.Set on the first block it sees it unset. This is deterministic across nodes, but it means BeginBlock writes to the params store at the first post-upgrade height; worth a brief note that this is intended and idempotent.

Comment thread sei-db/config/sc_config.go Outdated
// all_migrated_but_bank, migrate_bank, flatkv_only, test_only_dual_write, auto.
// defaults to memiavl_only.
// defaults to migrate_evm. While the NumKeysToMigratePerBlock gov param is 0
// (the default), migrate_evm is paused and produces the same app hash as

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 claim is not borne out by the routing code. In migrate_evm, MigrationManager.shouldForwardWriteToNewDB routes any caller write whose key is absent from memiavl directly to flatkv (migration_manager.go:431-435), and EVM creates brand-new keys (storage slots, accounts, receipts) every block. While paused (batch size 0) the boundary never advances, so shouldAppendLatticeHash returns false (composite/store.go:870-882) and flatkv is excluded from CommitInfo/AppHash. Result: paused migrate_evm commits new EVM state into flatkv that is outside the AppHash, whereas memiavl_only puts that same key into memiavl which IS in the AppHash — so the two AppHashes differ. This breaks the equivalence the rollout depends on (fork/halt risk in a mixed fleet) and leaves committed state uncovered by the merkle root. Please either route brand-new keys to memiavl while paused, or do not enable migrate_evm by default until the param is raised.

Comment thread sei-db/config/sc_config.go Outdated
return StateCommitConfig{
Enable: true,
WriteMode: types.MemiavlOnly,
WriteMode: types.MigrateEVM,

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] Changing the default WriteMode from MemiavlOnly to MigrateEVM is a consensus-relevant default flip. Combined with the node-local sc-write-mode override (app/seidb.go:109) and the AppHash non-equivalence of paused migrate_evm (see comment above), nodes that adopt this default will diverge from peers still on memiavl_only. Please confirm the upgrade story: existing nodes with an explicit memiavl_only in app.toml keep that value, but any node without it now silently enters migrate_evm. This needs to be coordinated as a network-wide switch rather than a per-node default.

// still route below, but no keys are pulled forward and no boundary
// metadata is rewritten, so the migration holds at its current cursor
// until the batch size is raised again.
advanceMigration := firstBatchInBlock && m.migrationBatchSize > 0

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] advanceMigration gates only the boundary advance and metadata writes; caller-write routing below (shouldForwardWriteToNewDB) still runs unconditionally, sending brand-new keys to flatkv even when paused. Since the boundary metadata is never written in the paused state, shouldAppendLatticeHash never latches, so those flatkv writes never enter the AppHash. This is the mechanism behind the AppHash divergence flagged on sc_config.go and matches Codex P1 #1. Consider also pausing the new-key forwarding (route everything to the old DB) while migrationBatchSize == 0, so paused migrate_evm is truly AppHash-identical to memiavl_only.

Comment thread sei-db/config/sc_config.go
Comment thread app/migration/params.go Outdated
Comment thread sei-db/state_db/sc/migration/migration_manager.go
Comment thread app/migration/params.go
seidroid[bot]
seidroid Bot previously requested changes Jun 30, 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 replaces the node-local sc-keys-to-migrate-per-block config with a consensus-deterministic governance param and is well-tested, but the migration kick-off still depends on each node's local sc-write-mode: a node that keeps a fixed memiavl_only config (the value existing app.toml files already carry after upgrade) cannot honor the gov trigger, its failed kick-off is only logged, and it will diverge from Auto-mode nodes once the param is raised. That residual per-node divergence axis plus the silent error handling are blocking for a consensus path.

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

Blockers

  • Upgrade/operational fork risk: DefaultStateCommitConfig.WriteMode changes from memiavl_only to auto, but already-running validators keep sc-write-mode = "memiavl_only" baked into their app.toml (rendered from the old default). The default change does not rewrite their config, so after upgrade many nodes will be fixed memiavl_only. When the gov param is later raised, Auto nodes migrate (changing the memiavl/flatkv contents that feed the AppHash) while fixed memiavl_only nodes silently fail the kick-off and never migrate — i.e. the chain forks. The PR's stated goal is to eliminate exactly this per-node divergence, but it introduces a new one (Auto vs fixed). At minimum this needs to be enforced (refuse to start / fail loud if a non-Auto store is in use while migration is/was requested) or made an explicit, hard upgrade prerequisite, not just relied upon operationally.
  • REVIEW_GUIDELINES.md is empty and codex-review.md is empty/absent, so no repo-specific review standards and no OpenAI Codex second-opinion pass were available for this synthesis (noting per instructions; not itself a code defect).
  • 2 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • app.go New() now unconditionally panics if CommitMultiStore() is not *rootmulti.Store. This is intentional fail-fast, but confirm no supported code path (export/CLI tooling, tests, legacy-store startup) constructs the App with a different commit multistore, since this turns a previously-guarded if ok into a hard panic.
  • Cursor P2 (ordering): applyMigrationBatchSize runs after forkInitializer and HardForkManager.ExecuteForTargetHeight. The kick-off's SetWriteMode rejects when any commitment store HasPendingChanges(). This appears safe because BeginBlock writes are buffered in the cache-multistore and only flushed into the commitment stores at Commit (so HasPendingChanges() should be false here), but it's worth confirming no hard-fork handler writes directly into a commitment store's pending buffer in the same block a positive param first applies.
  • MigrationManager.SetMigrationBatchSize writes m.migrationBatchSize without atomics; this is safe only because the manager is always wrapped by threadSafeRouter (write lock serializes it against ApplyChangeSets). Fine as-is, but the invariant is load-bearing and worth a brief assertion/test that migration routers are never used unwrapped.

Comment thread sei-cosmos/storev2/rootmulti/store.go Outdated
return fmt.Errorf("failed to set SC store migration batch size: %w", err)
}
if batchSize > 0 {
if mode, ok := rs.GetWriteMode(); ok && mode == sctypes.MemiavlOnly {

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] The kick-off fires whenever the effective mode is MemiavlOnly, but a composite store under a fixed memiavl_only config also reports GetWriteMode() == MemiavlOnly (see composite store.go:428-429 / resolveCurrentWriteMode). So for such a node this calls SetWriteMode(MigrateEVM), which returns the "write mode is fixed by configuration" error. Combined with the log-only handling in BeginBlock, that node never migrates while Auto nodes do — an AppHash divergence. The doc comment above ("every other SC store ... never reports a memiavl_only effective mode, so the kick-off is skipped") is inaccurate for fixed memiavl_only. Either special-case/skip non-Auto stores here, or treat the "fixed" failure as fatal so the divergence can't go unnoticed.

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.

For fixed MemiavlOnly node, we intentionally want to diverge the app hash and skip the migration. So the switch should only work for auto mode

Comment thread app/abci.go Outdated
numKeys = uint64(math.MaxInt64)
}
if err := app.rootStore.SetMigrationBatchSize(int(numKeys)); err != nil {
logger.Error("failed to set SC migration batch size", "err", err)

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] A failure to apply the consensus-relevant migration rate is only logged and execution continues. The level-triggered retry justifies swallowing transient failures (e.g. pending changes), but a permanent failure — chiefly a node pinned to a fixed sc-write-mode retained across upgrade — will silently fork from nodes that do migrate, one log line per block. Recommend distinguishing the non-retryable case (e.g. the "write mode is fixed" error) and halting/panicking, or validating at startup that the store is Auto when migration may be driven by governance.

Comment thread sei-cosmos/storev2/rootmulti/store.go Outdated
- type: eval
expr: NEW_ABCI_PARAM == "true"

- name: Test migration param change proposal should update NumKeysToMigratePerBlock

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.

it looks the migration gov tests triggers a real evm migration but only asserts the param value, not the migration. want to make sure it's on purpose

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.

There's no good way to check if real migration started or not, I decided to add a check to look for logs signals

Comment thread sei-db/config/sc_config.go
@yzang2019 yzang2019 added this pull request to the merge queue Jul 1, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 1, 2026
@yzang2019 yzang2019 enabled auto-merge July 1, 2026 05:13

@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.

Solidly-engineered change that moves the SC memiavl→flatkv migration rate from a node-local config to a governance param and makes auto the default write mode, with thorough tests and unusually detailed rationale comments. No correctness blockers found; the main risk is the "auto by default" upgrade path silently overriding explicitly-pinned non-memiavl write modes, which is documented but has no runtime guard.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Cursor second-opinion review (cursor-review.md) was empty — that pass produced no output.
  • Genesis-export gap (documented in app/migration/params.go): the migration subspace has no owning AppModule, so seid export omits NumKeysToMigratePerBlock; a chain bootstrapped from such a genesis silently re-seeds the default (0/paused) and operators must re-issue the proposal. Consider seeding it via the params ExportGenesis path or documenting it in operator runbooks so a mid-migration export doesn't silently reset the drain.
  • WriteMode resolution is now computed in two places — sei-cosmos/server/config/config.go:GetConfig and app/seidb.go:parseSCConfigs — both calling ApplyWriteModeAuto. They agree today; consider a shared helper or a test asserting parity so the two parse paths can't drift and produce a config divergence.
  • Codex finding #2 (app/abci.go applyMigrationBatchSize logs-and-continues on SetMigrationBatchSize failure, which can include the consensus-critical memiavl_only→migrate_evm kick-off): the author's in-code rationale is sound — a partial failure diverges AppHash and Tendermint halts the affected minority, while an all-fail keeps AppHash identical and the level-triggered kick-off re-fires. Acceptable as-is; flagging that Codex raised it and it was consciously resolved.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread sei-cosmos/server/config/config.go
@yzang2019 yzang2019 added this pull request to the merge queue Jul 1, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 1, 2026
@yzang2019 yzang2019 self-assigned this Jul 1, 2026
@yzang2019 yzang2019 added this pull request to the merge queue Jul 1, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 1, 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.

Well-structured, thoroughly tested, and heavily documented change that moves the consensus-relevant SC migration rate from a node-local config to a governance param read each BeginBlock. No correctness or security blockers found; the concurrency, overflow-clamp, determinism, and crash/restart paths are all covered by tests. Only minor non-blocking notes.

Findings: 0 blocking | 4 non-blocking | 0 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Second-opinion status: Codex reported "no material findings" but explicitly could not run any tests (the sandbox blocked the go1.25.6 toolchain download), so its pass is effectively static-only. The Cursor review file (cursor-review.md) is empty — that pass produced no output. Neither tool contributed findings to merge.
  • Log noise on opt-out nodes: applyMigrationBatchSize runs every BeginBlock, so a node pinned to fixed memiavl_only (sc-write-mode-enable-auto=false) hits the "skipping migration kick-off" logger.Info in rootmulti Store.SetMigrationBatchSize on every block once the gov param goes positive. In practice such a node diverges/halts on AppHash mismatch soon after the activation height, so exposure is bounded, but consider rate-limiting or edge-triggering that Info log to avoid per-block spam in the window before divergence.
  • Documented export caveat: the new migration subspace has no owning AppModule, so x/params ExportGenesis does not serialize NumKeysToMigratePerBlock. A seid export taken mid-migration omits the rate and a chain bootstrapped from it re-seeds the default (0, paused). This is clearly documented in app/migration/params.go, but it is worth surfacing in operator/runbook docs since a fork/recovery must re-issue the ParameterChangeProposal to resume the drain.
  • Behavior change: App.New() now panics if CommitMultiStore() is not *storev2/rootmulti.Store. This is intentional (storev2 is the only supported backend) and fine, but it converts a previously-tolerated legacy-store configuration into a hard startup failure — ensure no supported deployment/test path still constructs the legacy commit multistore.

@yzang2019 yzang2019 added this pull request to the merge queue Jul 1, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 1, 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.

Solid, well-tested, thoroughly documented change that moves the SC migration rate from a node-local config to a consensus-safe governance param and makes auto the default write mode. No correctness or compile-completeness defects found; the notes below are operational cautions and documented caveats.

Findings: 0 blocking | 4 non-blocking | 0 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Upgrade footgun (auto-default): WriteModeEnableAuto now defaults to true and an absent sc-write-mode-enable-auto key resolves to true, which silently overrides any explicit sc-write-mode. Old docs told operators that a flatkv_only node must stay flatkv_only forever; after this upgrade such a node (without sc-write-mode-enable-auto = false) flips to auto, which the PR's own toml comment says can fail every commit with a version-mismatch or serve reads from an empty memiavl. Not reachable in production today (no flatkv_only/evm_migrated nodes exist pre-migration) so not a blocker, but the release/upgrade notes MUST call out that any explicitly-pinned node needs to add sc-write-mode-enable-auto = false.
  • ExportGenesis omission (already documented in app/migration/params.go): the migration subspace has no owning module, so seid export does not serialize NumKeysToMigratePerBlock. A chain bootstrapped from a mid-migration export re-seeds the default (0/paused) and requires re-issuing the proposal. Acceptable and documented; consider a follow-up to include it in export or a runbook note for operators forking/recovering via export.
  • Second-opinion passes produced no findings: Codex reported no material findings but noted it could not run targeted Go tests (sandbox could not create the module cache / download the Go 1.25.6 toolchain — network blocked), so its pass was static-only; the Cursor review file was empty. REVIEW_GUIDELINES.md (from the base branch) was also empty, so no repo-specific standards were applied.
  • Nit: MigrationManager.SetMigrationBatchSize mutates m.migrationBatchSize non-atomically and relies entirely on the threadSafeRouter write lock for safety (correctly documented). Since ApplyChangeSets/Read are only serialized when wrapped by NewThreadSafeRouter, keep the invariant that migration-mode routers are always thread-safe-wrapped enforced/asserted so a future direct use can't introduce a data race under -race.

@yzang2019 yzang2019 added this pull request to the merge queue Jul 1, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 1, 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.

Solidly-designed, well-tested change that moves the SC-store migration rate from a node-local config to a governance param read each BeginBlock, correctly eliminating a per-node AppHash divergence risk. No blocking issues; the main note is an upgrade footgun where the new default-true sc-write-mode-enable-auto silently overrides explicitly pinned non-memiavl write modes.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Upgrade footgun (agrees with Codex, narrowed): sc-write-mode-enable-auto defaults to true and treats an absent key as true, so ApplyWriteModeAuto forces any node with an explicit sc-write-mode to auto on upgrade. This is safe for the current all-memiavl fleet and for evm_migrated nodes (auto correctly derives evm_migrated), but a flatkv_only-origin node (history began in flatkv, no memiavl) upgraded without first setting sc-write-mode-enable-auto=false will be flipped to auto — which the toml comment itself warns either fails every commit with a version-mismatch or silently serves reads from an empty memiavl. Recommend an explicit release/upgrade note calling this out, and consider failing loudly (or scoping the auto-fallback to absent/memiavl_only) when an explicit flatkv_only is present without the opt-out key.
  • Cursor's second-opinion review file (cursor-review.md) was empty — that pass produced no output. Codex produced a single finding, which is incorporated above.
  • app.go New() now hard-panics if the commit multistore is not *rootmulti.Store, whereas sibling call sites (trace snapshot) handle the !ok case gracefully. Verified safe for production since SetupSeiDB always prepends the storev2 rootmulti store as CMS, but the divergence in handling style is worth a brief comment for future maintainers.
  • Migration subspace has no owning AppModule, so its param is omitted from x/params ExportGenesis; a chain bootstrapped from a mid-migration seid export silently re-seeds the default (0/paused). This is documented in params.go but operators forking via export must re-issue the ParameterChangeProposal — worth surfacing in operator docs.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

Comments that couldn't be anchored to the diff

  • integration_test/gov_module/gov_proposal_test.yaml:929 -- [nit] Stray seid q gov proposal ... .status line sits between the node-0 and node-1 votes with no env capture and no node selector, so it does nothing. Looks like a leftover; remove it to keep the case tidy.

Comment thread app/seidb.go
// sc-write-mode-enable-auto key, and must still resolve to auto so a
// governance-driven migration can start without an app.toml edit. Only an
// explicit key flips it.
if v := appOpts.Get(FlagSCWriteModeEnableAuto); v != nil {

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] sc-write-mode-enable-auto defaults to true and an absent key stays true, so any node carrying an explicit sc-write-mode is forced into auto on upgrade. That's the intended behavior for the memiavl fleet (and evm_migrated is safely derived by auto), but a flatkv_only-origin node upgraded without pre-setting this to false gets silently flipped to auto — which the toml docs describe as unsafe (version-mismatch failures or empty-memiavl reads). Consider failing loudly for an explicit non-derivable mode (notably flatkv_only) that lacks the opt-out key, and/or add an explicit upgrade note. Non-blocking given the current all-memiavl fleet.

@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.

Solid, well-tested refactor that moves the SC migration rate from a node-local config to a deterministic governance param read each BeginBlock; concurrency, clamping, and determinism reasoning all check out. No correctness blockers — only operational and test-coverage notes remain.

Findings: 0 blocking | 7 non-blocking | 3 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Second-opinion passes: Codex reported "No material findings" and the Cursor review file (cursor-review.md) was empty — that pass produced no output.
  • Operational upgrade risk: WriteModeEnableAuto now defaults to true, which forces every node to auto and silently ignores an explicitly pinned sc-write-mode on upgrade. This is safe for the current all-memiavl fleet (auto derives memiavl_only), but a node legitimately pinned to flatkv_only/evm_migrated (e.g. from a flatkv-era state-sync) would break unless it adds sc-write-mode-enable-auto = false. It is documented in toml.go/sc_config.go, but deserves an explicit release-note / operator callout since it flips behavior for existing app.toml files with no edit.
  • Test-coverage gap: the actual production trigger — an auto node kicking off memiavl_only→migrate_evm when the gov param is raised from 0 — is covered only by rootmulti/composite unit tests. verify_flatkv_evm_migrate.sh boots nodes pinned (enable-auto=false) and flips sc-write-mode to migrate_evm via app.toml, then uses the gov proposal only for the rate, so the auto kick-off path is never exercised end-to-end in integration.
  • The genesis-export caveat (the migration subspace has no owning module, so seid export mid-migration omits NumKeysToMigratePerBlock and a re-import resets it to 0/paused) is acknowledged in params.go and is not consensus-fatal, but operators forking/recovering via export must re-issue the proposal — worth surfacing in operator docs.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread app/abci.go
// 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).
func (app *App) applyMigrationBatchSize(ctx sdk.Context) {
if app.rootStore == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] This if app.rootStore == nil guard is effectively dead code: New() panics unless the commit multistore is a *rootmulti.Store, so app.rootStore is always non-nil here. Harmless as defense-in-depth, but consider dropping it or adding a comment noting it can only be nil in a partially-constructed app (never in normal operation).

return StateCommitConfig{
Enable: true,
WriteMode: types.MemiavlOnly,
WriteModeEnableAuto: true,

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] Defaulting WriteModeEnableAuto to true (and treating an absent key as true) means every existing node flips to auto on upgrade, ignoring any explicitly pinned sc-write-mode. This is intentional and safe for the current all-memiavl fleet, but it silently overrides a pinned flatkv_only/evm_migrated node — which the toml comment warns will "fail every commit with a version-mismatch error or silently serve reads from an empty memiavl." Please ensure the upgrade release notes call this out prominently so operators of any legitimately-pinned node add sc-write-mode-enable-auto = false before upgrading.

# sei-node-0 vote yes
- cmd: seidbin=seid; chainid=sei; source integration_test/utils/_tx_helpers.sh && submit_tx_and_wait node_admin gov vote $PROPOSAL_ID yes --fees 2000usei
node: sei-node-0
# sei-node-1 vote yes

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] The # sei-node-1 vote yes comment sits above a stray seid q gov proposal ... .status query (line 62); the actual node-1 vote is on line 63. The intervening status query has no env binding so it's a harmless no-op, but the comment is misplaced — move it down one line (or drop the redundant query) for clarity.

@yzang2019 yzang2019 added this pull request to the merge queue Jul 2, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 2, 2026
@yzang2019 yzang2019 added this pull request to the merge queue Jul 2, 2026
Merged via the queue into main with commit 9f53204 Jul 2, 2026
68 of 69 checks passed
@yzang2019 yzang2019 deleted the yzang/add-migration-trigger branch July 2, 2026 02:35
wen-coding pushed a commit that referenced this pull request Jul 2, 2026
## 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.go` — `BeginBlock` 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.go` — `Committer` 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 fallback**
— `0` 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.

---------

Co-authored-by: Masih H. Derkani <m@derkani.org>
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.

4 participants