Skip to content

sn-manager: EVM preflight + auto-rollback (v2.6.1-testnet fix)#311

Merged
mateeullahmalik merged 1 commit into
masterfrom
fix/sn-manager-evm-preflight-and-rollback
Jul 8, 2026
Merged

sn-manager: EVM preflight + auto-rollback (v2.6.1-testnet fix)#311
mateeullahmalik merged 1 commit into
masterfrom
fix/sn-manager-evm-preflight-and-rollback

Conversation

@mateeullahmalik

Copy link
Copy Markdown
Collaborator

Summary

Adds an EVM-migration-aware preflight + auto-rollback branch to the sn-manager auto-updater. Ships as v2.6.1-testnet to remediate the 14-node crash-loop caused by v2.6.0-testnet on unmigrated (legacy secp256k1) fleet nodes.

Root cause / timeline

  • 08:24 UTC — v2.6.0-testnet published. Requires supernode.evm_key_name set in ~/.supernode/config.yml to start on a chain with the evm upgrade module active. Testnet chain has EVM active.
  • 08:40 UTC — sn-manager on 14 fleet SNs auto-upgraded v2.5.0-testnet → v2.6.0-testnet. New binary detects legacy secp256k1 keys + no evm_key_name, refuses to start. sn-manager restart-loops the crashed process.
  • 09:54 UTC — chain flips these SNs ACTIVE → POSTPONED after missed-report window. Fleet: 1 ACTIVE / 14 v2.5 → 0 ACTIVE / 0 v2.5 / 56 unreachable.

Immediate mitigation (already done): v2.6.0-testnet release drafted on GitHub to block new pulls. This PR is the permanent fix.

Invariant table

chain_has_evm evm_key_name current target expected
false "" v2.5.0-testnet v2.6.0-testnet allow
false "" v2.6.0-testnet v2.6.0-testnet allow (no rollback if no EVM)
true "" v2.5.0-testnet v2.6.0-testnet block
true "" v2.6.0-testnet v2.6.0-testnet rollback
true "" v2.6.0-testnet v2.6.1-testnet rollback (already stuck; rollback fires first)
true "evm-key" v2.5.0-testnet v2.6.0-testnet allow
true "evm-key" v2.6.0-testnet v2.6.0-testnet allow
true "" v2.5.0-testnet v2.5.1-testnet allow (below threshold)
true "" v2.4.5-testnet v2.5.0-testnet allow (below threshold)

Design

Two symmetric branches at the single decision point (updater.checkAndUpdateCombined):

A. Forward-block preflight — before installing the downloaded target:

  • Query supernode.config.lumera.grpc_addr for upgradetypes.ModuleVersions("evm")
  • If chain has EVM active AND target tag core-version >= v2.6.0 AND evm_key_name empty → ABORT
  • No symlink swap, no updates.current_version mutation
  • Write ~/.sn-manager/update-blocked.log with reason + timestamp + target + supernode_config_mtime
  • sn-manager status surfaces Update Blocked: true with reason + target + since

B. Auto-rollback — same code path, opposite direction:

  • If chain has EVM active AND current supernode >= v2.6.0 AND evm_key_name empty → stuck node
  • Fetch v2.5.0-testnet (hardcoded rollback target) from GitHub if not already installed locally
  • Activate via versionMgr.InstallVersion + SetCurrentVersion
  • Persist updates.current_version = "v2.5.0-testnet"
  • Write ~/.sn-manager/rolled-back.log + .needs_restart
  • sn-manager status surfaces Rolled Back: true with from/to/reason/at

C. Sticky-until-config-changes — after a block, subsequent check cycles compare current supernode config mtime against the mtime recorded in the block log; if unchanged, forward-check is skipped. Once the operator edits ~/.supernode/config.yml (adding evm_key_name), the mtime advances and the marker is auto-cleared on the next allow decision.

Fail-open on chain unreachability: a gRPC dial or ModuleVersions error returns preflightAllow with an INFO log. A transient chain outage must NEVER cause a block or a rollback.

Predicate-refactor pattern

decidePreflight(preflightInputs) → (preflightDecision, reason) is a pure function with no I/O and no logging. The runtime helper preflightCheck performs the I/O then delegates to decidePreflight. This is the invariant-first-coding §Anti-pattern 6 pattern — the invariant is table-driven testable without booting a network client.

Test matrix

All 9 rows of the invariant table PASS as table-driven cases in TestDecidePreflight_Table (internal/updater/preflight_test.go).

Additional coverage:

  • TestParseGRPCAddr — host/TLS inference for host:port, https://, http://, bare
  • TestQueryEVMModuleActive_FailOpenOnEmpty — empty grpc_addr → error → caller must fail-open
  • TestPerformRollback_HappyPath — pre-installed binary, symlink swap, config persist, .needs_restart, rollback log
  • TestWriteReadBlockLogMTime — round-trip mtime, clear
  • TestReadSupernodeEVMKeyName / TestReadSupernodeGRPCAddr — config-parse extensions
  • TestIsV260OrAbove — core-version comparison ignoring prerelease (v2.6.0-testnet treated as >= v2.6.0)

Local verification:

cd sn-manager
go vet ./...                                                    # OK
go test ./internal/updater/... ./internal/utils/... -race -count=1
# ok  ...updater  1.168s
# ok  ...utils    1.027s
cd ..
go vet ./...                                                    # OK

Deployment plan

  1. Merge this PR to master.
  2. Tag v2.6.1-testnet from master (a subsequent step, gated on this PR merge — NOT part of this PR).
  3. v2.6.0-testnet release stays drafted on GitHub (kept for history; no operator will ever pull it because the newer semver tag exists).
  4. Every existing sn-manager on a crashed node will pick up v2.6.1-testnet on its next 10-minute check cycle — the new sn-manager binary contains the rollback branch which will fire on the next iteration and revert supernode to v2.5.0-testnet, restoring the node to a runnable state until the operator completes their EVM migration.

Post-merge validation

Operator can run on any node:

sn-manager status
  • On a stuck node BEFORE this rolls out: output shows Current Version: v2.6.0-testnet and Status: Not running (process dead) in a loop.
  • On a stuck node AFTER v2.6.1-testnet picks up: output shows Current Version: v2.5.0-testnet, Rolled Back: true, plus from/to/reason.
  • On a fresh unmigrated node with v2.6.1-testnet+ as latest: Update Blocked: true with target + reason + migration doc link.

Explicit notes

  • MIN_SNMANAGER_VER mechanism is deferred to a subsequent PR per user directive. This PR relies on sn-manager already having auto-updated to v2.6.0-testnet on the crashed nodes (which it did), so v2.6.1-testnet ships as a normal semver-forward tag and is picked up by the existing check loop.
  • Preflight bypasses in cmd/{start,init,use,get}.go are intentional — those are operator-explicit code paths (sn-manager use vX, sn-manager get vX) where the operator has consciously requested a specific version. Comment in updater.go documents this.
  • The only auto-update forward path guarded by the preflight is AutoUpdater.checkAndUpdateCombined. The rollback branch is ALSO only reachable through this same function; no separate ticker or startup routine.

Cross-references

Rollback plan (for THIS PR, not the incident rollback)

If the preflight itself misbehaves in production, DO NOT revert-and-republish v2.6.0-testnet — that is what caused the outage. Instead cut a v2.6.2-testnet that reverts this commit; sn-manager on all nodes will pick up the semver-forward tag and revert to the pre-preflight behavior. Rollback log format is a stable text file (line-prefixed key: value); no on-disk migration is required.

Adds two symmetric branches to the sn-manager auto-updater's
checkAndUpdateCombined loop:

* Forward-block preflight — refuses to install a supernode target
  tag >= v2.6.0 when the chain has the evm upgrade-module active and
  ~/.supernode/config.yml has no supernode.evm_key_name set. The current
  binary is kept in place, the current_version in sn-manager config is
  NOT touched, and ~/.sn-manager/update-blocked.log is written with
  reason + supernode config mtime so a second check with unchanged mtime
  is a no-op (sticky until the operator remediates).

* Auto-rollback — when the CURRENTLY-INSTALLED supernode is already
  >= v2.6.0 on an evm-active chain with no evm_key_name (i.e. the node
  is in a crash loop from an earlier auto-update), fetches v2.5.0-testnet
  (hardcoded rollback target), activates it via versionMgr, persists
  updates.current_version, writes ~/.sn-manager/rolled-back.log and a
  .needs_restart marker.

Both branches only fire when the chain query succeeds. A gRPC-dial or
ModuleVersions error fails OPEN (allow) so a transient chain outage
cannot itself trigger a block or a rollback.

decidePreflight is a pure predicate over (chainHasEVM, evmKeyName,
current, target) and is covered by a table-driven test matching the
invariant table in the PR body. Operator-explicit code paths in
cmd/{start,init,use,get}.go intentionally bypass the preflight (the
operator asked for a specific version); this is documented in the
preflight comment block.

sn-manager status surfaces both markers by dumping the log contents
under 'Update Blocked:' / 'Rolled Back:' headers.

Deps: promotes google.golang.org/grpc + cosmossdk.io/x/upgrade to
direct requires to reuse upgradetypes.NewQueryClient (same call pattern
as supernode/cmd/evmigration.go).

Signed-off-by: Matee ullah Malik <mateeullahmalik@hotmail.com>
@mateeullahmalik mateeullahmalik merged commit d930cf5 into master Jul 8, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants