Consensus determinism replay

Run scripts/test-consensus-determinism.sh from any directory. The runner uses the Go 1.26.9 version pinned by go.mod, the caller's GOCACHE (CI's shared /go-build-cache; a dedicated /tmp cache when unset), a 15 minute limit on both the go test process and the test binary (-timeout 15m), limited build parallelism, and the shared build lock. It selects only TestConsensusDeterminism* in ./app; none of those tests depend on the SDK simulation flag or skip when it is unset. CI runs this script in the consensus-determinism job (long-test stage of .gitlab-ci.yml) and keeps test-quality-reports/consensus-determinism.log as an artifact.

The SDK simulation manager is built from modules implementing module.AppModuleSimulation. The registration test found auth, bank, denom, params, scheduler, and wasm. THORChain's AppModule does not implement that interface or register weighted operations, so TestAppStateDeterminism does not exercise THORChain swaps, observations, or outbounds. This suite uses a sanitized, fixed-time THORChain genesis fixture containing four funded pools (BTC, ETH, DOGE, LTC), two observed inbound transactions, a swap queue item, a scheduled outbound item, network fees, and a nonzero Reserve. Independent app instances import and commit that state eight times per seed. A fixed active validator supplies the required genesis validator update. Each run finalizes a block before committing and compares both the app hash and exported THORChain state. The app hash is the root of every module store, so a divergent write in any store fails the comparison; the exported THORChain JSON only makes a mismatch easier to read. Every second run reverses pool order, which should have no effect on keeper state. Go randomizes map iteration, but a small map takes some orders far more often than others. With two pools and six runs, a local mutation that wrote the last pool of a map iteration to a mimir escaped 1 in 5 suite runs; with four pools and eight runs per seed it failed 20 of 20. A one-unit pool change must produce a different committed app hash, proving the comparator detects divergence.

The suite also exports the THORChain state, reopens the committed database, and imports the export into a fresh app. At import, it checks that the THORChain records, the Reserve balance, and the BTC pool match the committed source state. It then finalizes and commits another block on two independent imports and compares their app hashes and exported THORChain state. Balances are not re-checked after that commit, because the block may legitimately spend from Reserve.

The migration test invokes the standard 3.20.0 upgrade handler with THORChain's module version map at 18 and checks that it reaches the current module version (AppModule.ConsensusVersion()), so later version bumps run their migrations too while the 18->19 effects are still checked. The handler runs on the root store's working state after FinalizeBlock, so Commit persists its writes. After the commit, the test reads committed state to check that the active validator version is 3.20.0 and the Reserve and BTC pool balances are unchanged. Before the upgrade it writes two legacy owner-keyed affiliate collector records in their pre-19 wire format, one for an owner with a THORName and one for an owner without, and funds the collector module to match. After the commit the named collector holds the first balance, the second owner received its RUNE, and both legacy keys are gone. Without those records the migration would return before writing anything. Two independent runs must produce the same post-migration hash and exported THORChain state. The hashes across software versions or across export/import are not assumed to match: only same-version, same-sequence committed hashes are compared.

The fixture imports persisted domain records through InitGenesis and runs ordinary block begin/end handlers. It does not submit signed user transactions, so it does not prove swap execution or outbound signing. Those paths need a separate transaction-level replay with validator and vault fixtures.

Validated on this workspace with scripts/test-consensus-determinism.sh: five tests passed, no skips (Go 1.26.9, go test -p 2 -timeout 15m ./app -run '^TestConsensusDeterminism' -count=1 -v).