Skip to content

Add v6.7 retired module upgrade tests - #4040

Open
alexander-sei wants to merge 12 commits into
mainfrom
alex/v67-upgrade-tests
Open

Add v6.7 retired module upgrade tests#4040
alexander-sei wants to merge 12 commits into
mainfrom
alex/v67-upgrade-tests

Conversation

@alexander-sei

@alexander-sei alexander-sei commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add build-tagged v6.7 upgrade coverage for the feegrant, capability, IBC, and transfer modules retired by v6.7, plus the deprecated oracle handlers.
  • Add boundary discovery, scaffolding, and structural checks so the current v6.6 -> v6.7 test set is selected from app/tags instead of being hard-coded in CI.
  • Exercise the v6.6 -> v6.7 boundary through two independent paths: a persisted two-process Go test and the existing live four-validator binary upgrade.
  • Add a dedicated GitHub workflow for the offline Go path while keeping the live-node workflow separate.

Cross-version coverage

  • The offline path compiles its source phase on release/v6.6, commits an application database, then compiles its target phase on release/v6.7 and reopens that same database.
  • The v6.7 target applies the real upgrade handler, removes exactly the retired module-version entries, retains oracle, and proves the seeded feegrant, capability, IBC, and transfer stores are unchanged after another reopen.
  • The live path builds one seid per ref, creates fixtures with v6.6, performs the governance halt and binary replacement across four validators, and runs the tagged v6.7 assertions against the same node homes.
  • In-process tests cover retired transaction behavior, fee-granter rejection, retained store data, exported genesis, orphaned module versions, and the remaining oracle blocker behavior.

Test plan

  • go test ./app -count=1 -run 'Test(V67|LatestUpgradeLeavesNoOrphanedModuleVersions|MountedStoresAreOwnedOrExplicitlyRetained|RetainedStoresRemainMounted|RetiredOracle)'
  • go test ./app -count=1 -run '^TestOracleKeepsWritingStateAfterV67$'
  • go test ./testutil/processblock/... -count=1
  • go test ./upgradetest/...
  • make upgrade-test-vet
  • FROM_REF=release/v6.6 TO_REF=release/v6.7 bash .github/scripts/offline-upgrade-test.sh
  • make fmtcheck

Made with Cursor

alexander-sei and others added 3 commits August 27, 2026 17:16
Cover the v6.7 removal of capability, feegrant, ibc and transfer, plus the
oracle handler deprecation, at levels that trade control for realism.

The existing tests call ApplyUpgrade directly, which never writes
upgrade-info.json and never reloads the stores, so App.SetStoreUpgradeHandlers
— the code deciding whether a store is dropped at an upgrade height — was
reached by nothing.

* Assert that transactions aimed at retired modules are rejected identically
  on both sides of the upgrade, that a rejected oracle transaction still pays
  its fee and occupies gas, and that retained store state survives the upgrade
  while being absent from an exported genesis.
* Guard the whole module version map rather than the names v6.7 drops, so a
  later removal that forgets DeleteModuleVersion fails there instead of
  leaving an entry on chain for the life of the chain. Require every mounted
  store to be owned by a registered module or declared retained with a reason.
* Drive a real governance upgrade on one throwaway node, reaching the halt,
  upgrade-info.json and store-reload path in seconds without docker.
* Run the upgrade across two real binaries, seeding a spendable fee allowance
  and tallied oracle rates with v6.6.2 where those modules are still alive.
  This is the only layer whose pre-upgrade state is real, because the current
  binary can no longer create it.
* Record how a live network answers before an upgrade and verify it after, as
  two phases with an artifact carried between them.
* Add the four-node docker suite and its CI matrix row, and check every YAML
  suite parses without needing a cluster.

processblock gains RunBlockDetailed and SignWithFeeGranter. Both are additive;
existing callers are unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
Extend the four-validator two-binary runner with real feegrant and oracle state plus v6.7 retirement assertions. Remove the overlapping local, YAML, and live-network harnesses.

Co-authored-by: Cursor <cursoragent@cursor.com>
Rely on the merged end-to-end test for upgrade orchestration and keep this branch limited to focused v6.7 in-process coverage and its test helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedSep 4, 2026, 12:51 AM

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 38.82225% with 561 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.16%. Comparing base (f9471b4) to head (e1fa6ad).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
upgradetest/crossversion.go 24.10% 455 Missing and 30 partials ⚠️
upgradetest/scaffold.go 80.85% 10 Missing and 8 partials ⚠️
upgradetest/set.go 58.97% 9 Missing and 7 partials ⚠️
upgradetest/boundary.go 81.69% 7 Missing and 6 partials ⚠️
upgradetest/cmd/boundary/main.go 60.60% 11 Missing and 2 partials ⚠️
upgradetest/cmd/new/main.go 55.55% 8 Missing and 4 partials ⚠️
testutil/processblock/tx.go 33.33% 3 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4040      +/-   ##
==========================================
- Coverage   61.33%   60.16%   -1.17%     
==========================================
  Files        2184     2082     -102     
  Lines      191492   179858   -11634     
==========================================
- Hits       117457   108218    -9239     
+ Misses      62951    61546    -1405     
+ Partials    11084    10094     -990     
Flag Coverage Δ
sei-chain-pr 50.12% <38.82%> (?)
sei-db 70.02% <ø> (+0.21%) ⬆️
sei-db-state-db ?

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

Files with missing lines Coverage Δ
app/upgrades.go 49.12% <100.00%> (+2.82%) ⬆️
testutil/processblock/common.go 78.57% <100.00%> (+0.52%) ⬆️
testutil/processblock/tx.go 76.27% <33.33%> (-4.87%) ⬇️
upgradetest/cmd/new/main.go 55.55% <55.55%> (ø)
upgradetest/boundary.go 81.69% <81.69%> (ø)
upgradetest/cmd/boundary/main.go 60.60% <60.60%> (ø)
upgradetest/set.go 58.97% <58.97%> (ø)
upgradetest/scaffold.go 80.85% <80.85%> (ø)
upgradetest/crossversion.go 24.10% <24.10%> (ø)

... and 109 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.

alexander-sei and others added 7 commits August 31, 2026 02:44
Coordinate tagged app assertions with real two-binary release runs so each minor boundary can validate both handler behavior and persisted state.

Co-authored-by: Cursor <cursoragent@cursor.com>
Compile phase-specific tests against v6.6 and v6.7 so CI verifies that the upgrade handler can reopen and migrate committed application state without starting nodes.

Co-authored-by: Cursor <cursoragent@cursor.com>
The v6.7 coverage asserted the upgrade's bookkeeping over one clean pass:
that four names left the module version map, with the retired stores
seeded by invented keys. That says nothing about the state and funds the
upgrade leaves behind, and nothing about the paths an operator actually
hits when an upgrade goes wrong.

The persisted layer now builds its fixture through the v6.6 keepers, so a
real fee allowance, capability owner set, IBC client, connection, channel
and transfer denom trace cross the boundary, and the assertions name the
key that vanished rather than reporting that a map differs. Escrowed
native coins and ibc/ voucher balances are covered too, since both live
in the bank module and outlive the transfer module that created them.
Removal is now checked against the stores themselves: the full committed
key/value set of every retired store, its presence in the commitment set
behind the application hash, and the version-map bytes in the upgrade
store, so a later StoreUpgrades{Deleted} cannot pass as a bookkeeping
change.

Three failure paths gain coverage. An upgrade interrupted before its
commit replays to a byte-identical application hash. An un-upgraded
binary halts at the plan height instead of committing under the old
module set. A new reopen phase runs the v6.6 code against the migrated
database and records what an operator rolling back will see: the
database opens and the retained stores are readable, but the application
hash has already diverged and the first block panics on the missing
v6.7 handler.

UPGRADE_TEST_SNAPSHOT_HOME points the retained-state assertions at a real
node home, since no fixture substitutes for a snapshot.

Co-authored-by: Cursor <cursoragent@cursor.com>
The cross-version test waited for each validator to reach a height and
then asserted it kept producing blocks. That is liveness: four nodes that
each keep building while disagreeing on state is exactly a chain split,
and every assertion passed on whichever node the callback queried. The
after callback now requires all four validators to report the same
application hash and the same block hash at the applied height and the
three above it, so a migration that is not deterministic shows up as a
disagreement naming the nodes and heights involved. A header carries its
parent's resulting hash, so the upgrade block's own output is the height
above the applied one, and the range covers it.

The plan name went unchecked because every test scheduled its plan from
the same constant the handler was registered from, which makes a mismatch
unrepresentable. A proposal naming v6.7.0 where the binary registers v6.7
halts every validator with no binary able to resume, since BeginBlocker
resolves handlers by exact name. Names built from app/tags are now
required to resolve to handlers, and near-misses derived from the real
name must halt instead of running it. That check is version-independent,
so it sits in the untagged file and covers future boundaries. The
persisted phases read the name back from the committed plan rather than a
shared literal, and the live callback takes it from the proposal the
chain recorded rather than the environment variable the runner intended.

Co-authored-by: Cursor <cursoragent@cursor.com>
Nothing after the upgrade sent a transaction, so the suite could pass on
a chain unable to execute one: every post-boundary assertion was a query,
a store read, or a rejection, and liveness was block production, which
continues on an empty chain.

Worse, the harness could not have noticed. Every seid broadcast here uses
--broadcast-mode sync, whose JSON is the CheckTx result, and the helper
checking it treated code 0 as success. A transaction admitted to the
mempool and then failing execution read as passing. The helper now takes
the hash from the broadcast, waits for the transaction to appear in a
block, and requires the delivered result, and the pre-upgrade seed
transactions go through it too. Ordinary bank and EVM sends now run on
both sides of the handler, so a failure separates an upgrade regression
from something that never worked here.

The harness configuration is pinned because it does not match the fleet.
Layers one and two run with OCC disabled and DefaultConcurrencyWorkers:
NewBaseApp never reads occ-enabled, and only the production command wires
SetOccEnabled, which app.Setup does not. The application-hash agreement
those layers establish is therefore agreement under a configuration
nobody operates, and that is now asserted at each construction site
rather than left to be rediscovered. The live validators are checked
against the file each container boots, on every node, and rechecked after
the binary swap. Pruning stays "nothing", which the suite's historical
queries depend on; the assertion records that as a known divergence from
a pruning validator rather than implying parity.

Co-authored-by: Cursor <cursoragent@cursor.com>
Running the four-validator boundary for the first time failed twice, in
ways no compile or single-process run could reach.

Coin amounts were parsed as int64. The localnet carries 5e21 usei, so
reading total supply aborted the before callback outright with a range
error, and the balance and fee-allowance readers had the same latent
fault waiting for any chain whose figures exceed an int64. Amounts now
flow as sdk.Int, which is the type the chain itself uses; the int64
helper stays only for block heights, where it belongs.

Restarting a validator diverted its output to a log of its own. The
orchestrator recognises an upgrade halt by grepping a node's original
log, so a validator this suite stopped and restarted before the upgrade
appeared to exit without halting, and the run died there. A restart now
appends to the log the cluster started the node with. An observed start
keeps a separate log, truncated per launch, because that path greps for a
halt and the node's original log already contains one from the
coordinated upgrade; sharing a file there would let a stale line stand in
for the halt under test.

Co-authored-by: Cursor <cursoragent@cursor.com>
CrossVersion.ReleaseBinary advertises the path at which a validator keeps
the binary it was running before the upgrade, but the runner created that
copy on the primary node alone. That held while the only caller exported
genesis through the node it already queried. It stops holding as soon as a
test puts the old binary back on some other validator, which is what
observing a rollback and an un-upgraded halt both require, and the run
fails inside the callback with a missing file rather than at the point the
assumption was made.

Staging it on each node makes the runner match what the harness offers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@masih

masih commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

This is plenty good! ship it alex!

@alexander-sei
alexander-sei marked this pull request as ready for review September 3, 2026 18:39

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

Large, well-structured addition of v6.7 upgrade coverage (in-process, persisted offline two-process, and live four-validator paths) plus boundary discovery so CI selects the test set from app/tags instead of a hard-coded version. No blocking correctness problems found; the notes below are about diagnosability of the new discovery steps and a compile-coverage gap for the offline phase files.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] .github/scripts/release-upgrade-test.sh drops the previous cross-validator height-spread assertion (maximum - minimum <= 3) from verify_post_upgrade and now only records each node's height. The replacement — CrossVersion.RequireBlockAgreement — is stronger, but it lives in the per-boundary tagged test, so a future Test*CrossVersion that forgets to call it loses the orchestrator's generic post-upgrade convergence check entirely. Consider keeping a cheap spread check in the orchestrator alongside the per-boundary assertions.
  • [suggestion] .github/workflows/offline-upgrade-test.yml paths: omits app/app.go, while release-upgrade-test.yml includes it. The offline test is the one that proves the retired stores stay mounted and readable across the boundary, and the mount list (kvStoreKeyNames) lives in app/app.go, so a change that unmounts a retained store will not trigger this workflow.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread Makefile Outdated
tag=$$(go run ./upgradetest/cmd/boundary tag) && \
tmp=$$(mktemp -d) && trap 'rm -rf "$$tmp"' 0; \
go test -list '^Test' ./app 2>/dev/null | awk '/^Test/ { print }' | sort > "$$tmp/base"; \
go test -tags "$$tag" -list '^Test' ./app 2>/dev/null | awk '/^Test/ { print }' | sort > "$$tmp/tagged"; \

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] Discarding stderr here means a compile failure in the tagged file is reported as no tests were added by build tag upgrade_v67 (line 657) with no compiler output. Since app/upgrade_v67_test.go is compiled by nothing else in a normal run, a build break in it is exactly the failure this target is most likely to hit, and the message points at the wrong cause.

The same pattern appears in .github/scripts/offline-upgrade-test.sh:138 ("$UPGRADE_TAG $phase phase selected no offline test") and .github/scripts/release-upgrade-test.sh:145 ("build tag $UPGRADE_TAG defines no Test*CrossVersion assertion").

Suggest capturing stderr and echoing it when the listing comes back empty, rather than sending it to /dev/null.

Comment thread Makefile
@set -e; \
for file in app/upgrade_v*_test.go; do \
[ -f "$$file" ] || continue; \
case "$$file" in *_offline_source_test.go|*_offline_target_test.go) continue ;; esac; \

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] upgrade-test-vet is described as the target that keeps already-shipped version-specific tests compiling, but it skips the two offline phase files. Nothing else compiles them either: go test ./... and golangci-lint cannot see them behind upgrade_v67 && offline_upgrade && upgrade_source|target, and offline-upgrade-test.sh only ever builds the current boundary's pair. So once v6.8 becomes the boundary, upgrade_v67_offline_{source,target}_test.go silently stop being type-checked and will rot.

The skip is understandable (the filename does not imply the full three-tag set), but the tag set is derivable — e.g. tag=$(basename "$file" _offline_source_test.go) plus ,offline_upgrade,upgrade_source — so these could be compiled too rather than excluded.

Comment thread app/upgrade_offline_harness_test.go Outdated
require.Equal(t, want, resp.Value, "query /%s/key returned a different value", storeName)
require.NotNil(t, resp.ProofOps, "%s is missing from the commitment set", storeName)
require.NotEmpty(t, resp.ProofOps.Ops, "%s is missing from the commitment set", storeName)
return

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 unconditional return at the end of the first iteration means this proves exactly one key — and which key is Go map iteration order, so it differs run to run. The function reads as though it proves every entry in snapshot, and its name says nothing about sampling.

Either drop the return and prove all keys, or make the intent explicit (pick a deterministic key, e.g. the lexicographically smallest, and say so in the doc comment). As written it would also trip staticcheck SA4004 if run.tests were ever enabled.

Comment thread app/upgrade_orphan_test.go
This commit hardens upgrade validation by surfacing failures in app/test discovery, enforcing post-upgrade validator synchronization, and compiling historical offline upgrade phases against their release refs to avoid API mismatches. It also makes the offline proof check validate the lexicographically first snapshot entry and resets the upgrade registry after asserting orphaned module versions are absent.

@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 1 potential issue.

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 e1fa6ad. Configure here.

commit="$(resolve_release "$version")" ||
die "unable to resolve release/$version"
git -C "$REPO_ROOT" worktree add --detach "$worktree" "$commit" >&2
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Vet target needs unfetched release refs

Medium Severity

make upgrade-test-vet now builds offline phases in worktrees of release/vMAJOR.MINOR, but the Go Test job that runs that target still checks out with fetch-depth: 1 and never fetches those branches first. resolve_release then has to git fetch origin release/v6.6 from a shallow clone, which can fail on pull-request checkouts (especially forks) or become a full-history fetch, so the compile step errors even when the tests themselves are fine.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e1fa6ad. Configure here.

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.

2 participants