feat(gnoland): chain hardfork mechanism v3#5511
Conversation
- Add ChainID field to GnoTxMetadata for tx provenance recording - Add InitialHeight validation (non-negative) to GenesisDoc.Validate and ValidateAndComplete - Add test cases: no chain ID override when BlockHeight=0, no override when OriginalChainID unset - Update ADR: document per-tx vs state-level design choice, mark InitialHeight as implemented end-to-end
…lay and ValidateAndComplete
…is.sh misc/hardfork/ — new Go binary with three source modes: - RPC: iterates blocks from a live/halted node, extracts txs with metadata - local dir: reads genesis.json + txs.jsonl from a stopped node data dir - genesis file: single .json source (no tx history) Produces a hardfork genesis with: - chain_id updated to new chain - initial_height set to halt_height + 1 (both at GenesisDoc level and app_state) - original_chain_id set for historical tx signature verification - historical txs appended with BlockHeight/Timestamp/ChainID metadata misc/deployments/gnoland-1/generate-genesis.sh is now a thin wrapper around hardfork genesis with gnoland-1 specific defaults (chain IDs, overlay directory).
…o worktree-hf-framework-5411
…ay smoke-test Adds a new 'hardfork test' subcommand that loads a hardfork genesis.json into an in-memory gnoland node and replays all transactions in-process. Key behaviors: - Generates a fresh single-validator identity (replaces genesis validators) so the node can produce blocks without requiring real validator keys - SkipGenesisSigVerification enabled for genesis-mode txs - Historical txs (block_height > 0) go through the normal ante handler using original_chain_id from genesis for signature verification - Progress reporting every 30s for long replays - --verbose flag logs each tx result - --keep-running flag keeps the node alive for manual RPC inspection - Exit code 0 on success, non-zero on failure Also adds: - Unit tests covering error paths and a full empty-genesis replay - Makefile 'preview-and-test' target for quick local smoke tests - Updated 'hardfork genesis' next-steps to reference 'hardfork test'
- Add ChainID field to GnoTxMetadata for tx provenance recording - Add InitialHeight validation (non-negative) to GenesisDoc.Validate and ValidateAndComplete - Add test cases: no chain ID override when BlockHeight=0, no override when OriginalChainID unset - Update ADR: document per-tx vs state-level design choice, mark InitialHeight as implemented end-to-end
…lay and ValidateAndComplete
Use a []string PastChainIDs on GnoGenesisState instead of a single OriginalChainID. The chain ID override during genesis replay now uses the per-tx metadata.ChainID, verified against the allowlist. This supports multi-hop upgrades where a genesis contains txs from multiple past chains, and prevents silent overrides for unrecognised chain IDs.
…amp != 0 Timestamp=0 in metadata should not clobber the genesis block time with Unix epoch. Guard the override the same way BlockHeight is guarded. Also adds a test case covering the zero-timestamp fallback.
- TestIsPastChainID: table-driven test for the allowlist helper - multi-chain replay: two txs each signed with a different past chain ID, validating the core PastChainIDs use case - ADR: clarify GnoGenesisState.InitialHeight is informational only, reflow to 80 cols
… test Two keys with BlockHeight>0 caused UnauthorizedError because the second key got accNum=1 but was signed with accNum=0 (createAndSignTx hardcodes 0,0). Use one key for both txs; sign tx2 with seq=1 (incremented by tx1).
- Simplify isPastChainID to use slices.Contains - Fix panic in reconstructLastCommit when InitialHeight > 1 and block store is empty (fresh genesis with InitialHeight set by Handshaker) - Add TestReconstructLastCommit_InitialHeight to cover the fixed path
When GenesisDoc.InitialHeight > 1, the Handshaker sets state.LastBlockHeight = InitialHeight - 1 after InitChain, but the block store is still empty (Height() == 0). NewBlockchainReactor was panicking with "state (N) and store (0) height mismatch" in this case. Fix: skip the mismatch check when the store is empty. A non-empty store must still match state exactly. Add TestNewBlockchainReactor_InitialHeight that reproduces the panic before the fix and passes after.
…d SDK
Replace all `height == 1` / `height > 1` genesis-detection heuristics with
semantically-correct checks so that a chain starting at InitialHeight > 1
(replaying historical transactions at genesis) behaves identically to a
standard height-1 chain.
Locations fixed (test written first for each new panic found):
consensus/state.go
- reconstructLastCommit: skip when block store is empty (InitialHeight > 1)
- needProofBlock: treat empty store as genesis
- createProposalBlock: use empty commit when store is empty
blockchain/reactor.go
- NewBlockchainReactor: allow state.LastBlockHeight != store.Height when
store is still empty (Handshaker sets LastBlockHeight = InitialHeight-1)
bft/types/block.go
- ValidateBasic: detect genesis via `Height == 1 || LastBlockID.IsZero()`
instead of `Height > 1`; preserves backward compat for malleated height-1
blocks while correctly handling InitialHeight > 1
state/state.go
- MakeBlock: use commit.BlockID.IsZero() to select genesis time
state/validation.go
- ValidateBlock: replace Height == 1 checks with isGenesisBlock derived
from state.LastBlockID.IsZero()
state/execution.go
- getBeginBlockLastCommitInfo: skip validator load when LastCommit has no
precommits (genesis block has an empty commit regardless of height)
store/store.go
- SaveBlock / saveBlockPart: allow first save at any height when store is
empty (contiguity still enforced once store is non-empty)
sdk/baseapp.go
- validateHeight: allow first BeginBlock at any height when prevHeight == 0
(InitialHeight > 1 sends height=InitialHeight, not height=1)
Tests added:
- gno.land/pkg/gnoland: TestNodeBootWithInitialHeight (full in-memory node)
- consensus/replay_test: TestReconstructLastCommit_InitialHeight,
TestNeedProofBlock_InitialHeight, TestCreateProposalBlock_InitialHeight
- blockchain/reactor_test: TestNewBlockchainReactor_InitialHeight
- state/execution_test: TestGetBeginBlockLastCommitInfo_InitialHeight
- store/store_test: TestBlockStore_InitialHeight,
TestBlockStore_ContiguousAfterInitialHeight
- sdk/baseapp_test: TestBeginBlock_InitialHeight
# Conflicts: # gno.land/pkg/gnoland/app.go # gno.land/pkg/gnoland/app_test.go # gno.land/pkg/gnoland/types.go # tm2/pkg/bft/consensus/replay_test.go
…cation Record each signer's account number and pre-tx sequence in GnoTxMetadata so historical transaction signatures verify correctly during hardfork genesis replay, even when prior txs are skipped or produce different results due to VM fixes. Changes: - Add SignerAccountInfo type with Address, AccountNum, Sequence fields - Add Failed bool and SignerInfo []SignerAccountInfo to GnoTxMetadata - Add NewAccountWithNumber to AccountKeeper (creates account with specific number, updates global counter to prevent collisions) - Genesis replay loop: force-set account state from SignerInfo before Deliver, skip failed txs - Rewrite source_rpc.go export: include failed txs, query account numbers via RPC, single-pass sequence tracking with brute-force recovery for ambiguous failed txs (ante-fail vs msg-fail) - Update ADR with SignerInfo, Failed, and sequence resolution algorithm - Fix OriginalChainID → PastChainIDs in test_test.go Tested end-to-end: exported 2637 txs (19 failed) from gnoland1, replayed in-memory with 0 signature verification failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Promote v3 pkg path + assertion func name to package-level consts in gnoland/app.go (validatorsV3PkgPath, assertGenesisValopersFunc, missingV3PkgPanicSubstr); the recover-substring match now derives from the constant instead of a duplicated inline string. - Demote shouldAssertValoperCoverage from method-on-cfg to free function (the receiver was unused); update the test accordingly. - Drop the redundant "assertion panic:" prefix in the recover fallback — the outer InitChainer wrap already prefixes "genesis valoper coverage assertion failed:". - Trim narration-style comments in proposal_test.gno, valoper_seed.go, and valoper_seed_test.go per CLAUDE.md "default to no comments unless WHY is non-obvious". - Rename TestNewValidatorProposalRequest_PhantomBaselineWouldGhost → ...PhantomBaselineDocumentsUnreachableState so the test name signals it pins documented unreachable behavior rather than asserting desired behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
b54675b to
c51dfd0
Compare
|
My may 1st two commits improve the existing approach. The following set of commits are for adding valoper signing address rotation. Valoper signing-key rotation, layered on top of the hardfork-replay infrastructure Adds the realms + tooling needed for operators to rotate their consensus signing key without going through GovDAO Realms
Tooling (gnogenesis fork)
gnoland integration
Tests
Stats 33 files, +3323 / −824 lines across examples/gno.land/r/{gnops/valopers,sys/validators/v3,sys/params}/, Dependencies Builds on this branch's existing hardfork-replay infrastructure: PastChainIDs, --migration-tx, gnogenesis fork |
Inline the rationale at each callsite rather than cross-referencing an out-of-tree plan file. Affected: - examples/gno.land/r/sys/validators/v3/validators.gno - examples/gno.land/r/sys/params/valoper.gno - examples/gno.land/r/gnops/valopers/admin.gno - examples/gno.land/r/gnops/valopers/proposal/proposal_test.gno Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
c51dfd0 to
f1c67b7
Compare
The hardfork-mode auto-run of v3.AssertGenesisValopersConsistent fires spuriously under gnogenesis fork test: the smoke-test path replaces genDoc.Validators with a fresh MockPV whose signing addr is never registered as a valoper, so the assertion sees an uncovered genesis validator and aborts boot. The existing TestExecTest_HardforkGenesis didn't catch this because its minimalAppState has no v3 deployed — the missingV3PkgPanicSubstr recover branch swallows the panic. Once v3 is actually deployed (the real production case), the recover branch is bypassed and the assertion fails legitimately. Add a SkipValoperCoverageAssertion flag on InitChainerConfig that gates the call site before the helper runs. fork test sets it to true, so its synthetic-validator boot path no longer trips the assertion regardless of v3 deployment. Also fix the v3 godoc that still claimed valoper-seed emits the assertion as a tail tx — it doesn't (the InitChainer auto-run replaced it). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tale doc - Extract InitChainerConfig.shouldRunValoperCoverageAssertion(req) combining the cfg override with the request-level gate. Lets the flag composition be unit-tested directly without a full BaseApp setup. Add TestInitChainer_SkipValoperCoverageAssertion covering the flag-true / flag-false matrix. - Drop the dangling "Mirrors the existing v3 NewProposalRequest convention" line from NewValidatorProposalRequest's godoc — the legacy NewProposalRequest was removed earlier in the branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Future-feature ADR documenting the checkpoint mechanism for
faithful cross-fork valset history replay. Pieces:
- Bare-metadata checkpoint txs (Tx=nil; Metadata{Valset, Valrealm}).
- Per-realm ImportValidators(snapshot) entrypoints, gated on a new
ExecContext sentinel so the privileged-import semantics are
unforgeable.
- Replay-time apply: hard-set valset:current + sync the named
realm's local state to match the snapshot.
Lifts the current "no historical valoper/v3 txs in migration .jsonl"
constraint by giving each historical chain segment a valset:current
that matches the historical state at that segment's epoch.
Deferred until the second hardfork (the one where the source chain
already has v3+valopers history worth preserving). The immediate
gnoland-1 ceremony uses the simpler re-bootstrap-via-valoper-seed
pattern, which is sufficient and out of scope here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- gofmt app.go and valoper_seed.go (column alignment + multiline import). - t.Parallel() in subtests of TestShouldAssertValoperCoverage, TestInitChainer_SkipValoperCoverageAssertion, TestValoperSeed_RejectsBadMoniker to satisfy the tparallel linter.
1. v3/proposal: ExecutorVanishedCacheEntryPanics. The 'operator vanished from valoperCache between propose and execute' branch is unreachable through the public API today (only Set is called by NotifyValoperChanged), but bptree exposes Remove so the underlying data structure does support deletion. The test confirms the panic IS reached when the cache entry disappears between propose-create and propose-execute, which keeps the defensive check meaningful as defense-in-depth: a future commit introducing a delete path will not silently mis-publish an empty/wrong valset. 2. tm2/sdk: BeginBlock_NoStatelessContiguityGuard. After the InitialHeight-as-first-class-state refactor (validateHeight removed), BaseApp accepts non-contiguous BeginBlock heights at the SDK layer. Contiguity is enforced upstream by the consensus engine via state.ValidateBlock and the BlockStore. The test pins the resulting BaseApp behavior so a future regression that reintroduces a stateless guard surfaces, and conversely flags any future change that depends on BaseApp catching it.
…stateless-guard contract on BaseApp.BeginBlock valopers.Register: post-genesis squat guard now requires PreviousRealm to be a pure EOA (IsUserCall) AND OriginCaller==addr. Matches the same-shape guard used by r/sys/namereg/v1.Register elsewhere in this PR. The OriginCaller==addr check alone was strictly weaker: relay-style flows (a third realm forwarding a user's tx) and 'maketx run' ephemeral realms both leave OriginCaller==operator while PreviousRealm() points at code, so they could pass the old check. Genesis-mode bypass (ChainHeight()==0) is unchanged. baseapp: expand the BeginBlock doc comment so the contract is clear to embedders driving BaseApp without a real consensus engine. Pinned behavior is in TestBeginBlock_NoStatelessContiguityGuard.
Conflict resolution:
- examples/gno.land/r/gnops/valopers/valopers.gno: take ours. Master added
an IsUserCall guard to the legacy fee-check path; that path no longer
exists in VALOPLAN2 (fee was moved to sysparams) and the equivalent
IsUserCall guard now lives in the squat-guard at Register's entry,
applied to all calls instead of only fee-bearing ones.
- gno.land/pkg/integration/testdata/{params_valset_proposal_*,valopers}.txtar:
take ours (gas budgets are calibrated for the VALOPLAN2 signingRegistry +
v3 cross-call) then apply master's chore from #5630 (drop the unneeded
-broadcast flag) in a follow-up sed pass.
…gas table Master added per-native gas calibration in #5629. The two test-only sysparam setters (uint64/int64) introduced by VALOPLAN2 weren't yet registered, so any gno test exercising them (e.g. r/gnops/valopers/valopers_test) panics with 'no calibrated gas entry'. Add both to the test-stdlib table at zero gas.
….txtar The IsUserCall tightening I added in dfdf648 was overzealous for this surface and broke legitimate `maketx run` flows used by the 4 valoper integration txtars (params_valset_proposal_*, valopers). Why OriginCaller==addr is sufficient (without IsUserCall): identity-squatting requires the attacker to satisfy OriginCaller==victim, which already requires the victim's signing key. r/sys/namereg/v1.Register gates on IsUserCall because IT reads banker.OriginSend() for an anti-squatting payment and needs the envelope to reflect what landed at this realm vs a phantom payment from a previous frame. valopers.Register has no per-call payment receipt check tied to OriginSend semantics in the same way, so IsUserCall would only block legitimate operator-authored `maketx run` scripts without adding identity-squat protection. Comment expanded to explain the asymmetry. Bump valopers.txtar:49 gas-wanted from 35M to 60M for the new instructions proposal — VALOPLAN2 added cross-call paths that the original budget no longer accommodates.
…esAt cap to ~4 years; add gnokey --expires-at flag with 'none' keyword Protocol change (consensus rule): tm2/pkg/std/account.go now defines two constants where there was one. MaxSessionDuration = 4 * 365 * 24 * 60 * 60 // ~4 years (was 30 days) MaxSpendPeriod = 30 * 24 * 60 * 60 // 30 days (new) MaxSessionDuration caps non-zero ExpiresAt; MaxSpendPeriod caps SpendPeriod. ExpiresAt = 0 (no expiry) remains exempt from the cap — the cap exists to catch bad input (typos, milliseconds-vs-seconds confusion), not to enforce a hard lifetime ceiling. SpendLimit remains the authoritative bound on damage from long-lived sessions. Splitting the two constants reflects that they're independent concepts: a session can outlive any single spend window. Bumping the lifetime cap to ~4 years matches typical hardware-key replacement cadence and removes the 30-day ceiling as a forced re-auth event for long-lived agent / device-login sessions. Mixed-version validators on the same chain-id will diverge for MsgCreateSession with ExpiresAt in (now+30d, now+4y]. Ship via the fork-replay mechanism (PR gnolang#5511), not as a soft rollout. CLI hardening (tm2/pkg/crypto/keys/client/session_create.go): - New --expires-at flag, REQUIRED. Empty errors with a clear message. - 'none' keyword for the explicit no-expiry choice (so unbounded sessions can't be created accidentally). - Duration accepts h/m/s plus 'd' (days) and 'w' (weeks) with float prefixes (0.5d, 1.5w). - Bare integers must be future unix timestamps within the cap; rejects ambiguous small values like '7' that would silently become 1970-era timestamps. - Client-side cap matches the chain (MaxSessionDuration) so users fail fast instead of paying gas on a tx the chain will reject. - Overflow-safe: parseDurationSeconds works in int64 seconds via float64 math, clamping NaN/Inf/overflow to int64 limits so the upper-bound check rejects rather than computing a wrapped value. Test additions (session_create_test.go): 21 cases covering empty error, 'none', durations (compound, fractional, exact-cap, just-over, way-over), unix timestamps (future-within-cap, past, too-far, negative, ambiguous-bare-int), and direct overflow probes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Overview
Chain hardfork mechanism for gno.land: export all state and historical transactions from the source chain, replay them during
InitChainon the new chain, and start producing blocks at the halted height. Replaces the original single-OriginalChainIDdesign from #5411 with a more flexible multi-chain model (PastChainIDsallowlist + per-txChainID).History:
What's in
tm2 (consensus + SDK)
GenesisDoc.InitialHeightconsensus starts block production at this height afterInitChain;Handshakersetsstate.LastBlockHeight = InitialHeight - 1.BlockchainReactor,state,store, validation all updated to handle chains whereInitialHeight > 1(empty block store, non-contiguous block save, validator set / consensus params persisted at InitialHeight, etc.)BaseApp.lastBlockHeighttracker (this iteration): real chain height =multistoreVersion + initialHeightOffset, with the offset persisted undermainInitialHeightKeyand restored on every restart.validateHeightnow enforces strict contiguity against real chain height; the previous "allow monotonic jump" branch (which permanently bypassed contiguity forInitialHeight > 1chains) is gone.BaseApp.Infoguard handle calls before the multistore is loaded.auth.SkipGasMeteringKeycontext flag that letsSetGasMeterbypass the new VM's gas meter (used forGasReplayMode="source").RequestInitChain.InitialHeightnew ABCI field so the app can cross-check againstGnoGenesisState.InitialHeight. Amino round-trip test added.gno.land
GnoGenesisStateextensions:PastChainIDs []stringallowlist of past chain IDs valid for signature verificationInitialHeight int64cross-checked againstGenesisDoc.InitialHeightGasReplayMode string""/"strict"(default, new VM's gas meter) or"source"(bypass gas meter, preserve source-chain outcomes)GnoTxMetadataextensions:BlockHeight int64original block heightChainID stringoriginating chain IDFailed booltx had non-zero return code on source chain (skipped during replay)SignerInfo []SignerAccountInfoper-signer account metadata (address, account number, pre-tx sequence) so signatures verify correctly even if earlier txs divergedGasUsed,GasWanted int64source-chain gas (populated by tx-archive, used by replay report)auth.NewAccountWithUncheckedNumber(this iteration, renamed fromNewAccountWithNumber): create accounts with a specific number, bypassing the auto-increment counter. Doc comment now spells out the precondition that the caller must enforce uniqueness; the rename forces every call site to acknowledge it.validateSignerInfopreflight (this iteration): scans everySignerInfoentry across all txs at the start ofloadAppState. Rejects the genesis if two different addresses claim the same account number, or if aSignerInfoclaims a number reserved by a balance-init account at a different address. Defense-in-depth against a malformed genesis silently corrupting state.InitChainerConfig.StrictReplay(this iteration): opt-in fail-closed boot. Defaults tofalsefor backwards compat. Hardfork operators set it totrueso any non-skipped tx replay failure abortsInitChaininstead of letting the chain boot in a corrupted state. Skipped txs (metadata.Failed = true) do not count.BlockHeight == 0) use the firstPastChainIDsentry for sig verify when a hardfork is in progress (PR feat: hardfork-replay improvements (BaseApp InitialHeight + PastChainIDs genesis-mode + --patch-realm) #5540). The genesis-mode chain-ID branch is now gated onmetadata == nil(this iteration) so migration txs (metadata != nil,BlockHeight == 0,Timestamp != 0) keep their metadata-drivenctxFninstead of being silently overwritten.BaseApp.InitChainerror surfacing (this iteration): whenInitChainerreturnsResponseInitChain.Error, return cleanly instead of falling through to the validators-count sanity check, which would otherwise panic with a misleading"validators count mismatch"and mask the real cause.InitChain:ok/ok_gas_differs/failed/skipped_failed. ExposesOutcomes()andFailedCount()for external tooling.Hardfork tooling (
contribs/gnogenesis/internal/fork/)gnogenesis fork generategenerate a hardfork genesis from a source chain (RPC URL, local data dir, or exported tarball).gnogenesis fork testlocal genesis replay smoke-test.--patch-realm PKGPATH=SRCDIR(repeatable) rewrite a genesis-modeaddpkgtx in-place with files fromSRCDIR. Lets you deliver realm upgrades as part of the fork (e.g. adding a new.gnofile to an existing realm) since you cannot re-addpkg post-deploy (PR feat: hardfork-replay improvements (BaseApp InitialHeight + PastChainIDs genesis-mode + --patch-realm) #5540).--migration-txinject a single migration tx at the end of the historical replay.bruteForceSignerSequenceresolve signer sequences during export by trying candidate values against the signature.Bugs found and fixed during review
tm2 consensus (all fixed)
BlockPoolstarted atstore.Height()+1 = 1instead ofstate.LastBlockHeight+1 = InitialHeight. Nodes trying to fast-sync would request non-existent blocks.saveStateonly saved validators whennextHeight == 1. With InitialHeight > 1,LoadValidatorsfailed andLoadConsensusParamspanicked at block InitialHeight+1.ValidateBasicbypass via zeroedLastBlockIDany block withLastBlockID.IsZero()could skip commit validation. Fixed: only allow skip when commit is also nil/empty.BaseApp.validateHeightpermanent contiguity bypass the previous "allow monotonic jump" branch compared real block height against the multistore version. After the first commit,actual > prevHeightis trivially true on every subsequent block, so the contiguity check was bypassed forever (an attacker or buggy consensus engine that skipped N blocks would be silently accepted). Fixed by tracking real chain height inlastBlockHeight(this iteration).BaseApp.InitChainmasking real error whenloadAppStatereturned an error response, the validators-count sanity check fired with"validators count mismatch"masking the actual cause. Fixed: return cleanly on error response (this iteration).gno.land (all fixed)
loadAppStatereturns nil even on N tx failures chain booted in a corrupted state when historical-tx replay had failures. Fixed via opt-inStrictReplayinInitChainerConfig(this iteration).ctxFnoverwrite the genesis-mode chain-ID branch fired on anymetadata.BlockHeight == 0, stomping the metadata-drivenTimestampoverride on migration txs. Fixed: tighten predicate tometadata == niland compose with any priorctxFn(this iteration).NewAccountWithNumberhad no SignerInfo collision check twoSignerInfoentries with the sameAccountNumbut different addresses, or aSignerInfocolliding with a balance-init account, would silently zero the original account's balance. Fixed: rename toNewAccountWithUncheckedNumber(forcing every call site to acknowledge the precondition) plusvalidateSignerInfopreflight inloadAppState(this iteration).ResponseDeliverTxwas empty (looked like success) explicit error marker so indexers can distinguish.GnoGenesisState.InitialHeightwasn't cross-checked againstGenesisDoc.InitialHeightaddedInitialHeighttoRequestInitChainand validate inloadAppState.RequestInitChain.InitialHeighthad no amino round-trip test silent registration regression would only surface during a real hardfork (this iteration).Hardfork tooling (fixed)
applyOverlaysilent no-op listed scripts but didn't execute them, returned success. Fixed: returns error when scripts found but execution not implemented.encoding/jsoninstead of amino interface types (std.Msg) lost on round-trip. Fixed: both writer and reader now use amino.verifyGenesisFilefailure returned success tool could produce invalid genesis and exit 0. Fixed: failure aborts (opt out with--no-verify).bruteForceSignerSequencefixed: 10 table-driven tests.Docs linter (side fix for green CI)
staging.gno.land,archive.org, and add retry/timeout logic so transient remote-link failures don't block unrelated PRs.Still open (design / follow-up)
contribs/gnogenesis/internal/fork/source_rpc.go) a single transient error during tx fetch aborts everything; needs exponential backoff + checkpointing. Architectural, follow-up PR.queryAccountAtHeightsilent nil all error paths return nil with no indication; flaky RPC → wrong sequence metadata.Cherry-picked from #5597 (this iteration)
Three follow-ups originally staged in the master-based hardfork series, brought back to where they belong since they modify or extend code introduced here:
1babfe42afix(consensus): skip phantom heights during replay when InitialHeight > 1— ABCI handshake replay path used to assume heights[1, appBlockHeight+1]always have a stored block; for chains starting atInitialHeight > 1, heights belowInitialHeightnever had blocks and replay errored with "block not found for height 1".5bf2fa53efix(gnogenesis): default gas-storage params and gas_replay_mode in hardfork genesis—buildHardforkGenesisnow defaults the post-fix(tm2,gnovm,gno.land): gas storage #5415vm.paramsgas-storage fields fromvm.DefaultParams()when the source has them all at zero, and setsgas_replay_mode = "source"when unset. Operator overrides preserved. 4 unit tests.e31268467feat(gnogenesis): add --skip-failing-genesis-txs and --skip-genesis-sig-verification flags to fork test—make smoketestnow matches what production validators actually run.End-to-end validation
The hf-glue testbed (#5486) runs
make fetch && make init && make upagainstrpc.gno.landhalt@704052 and produces a 192 MB hardfork genesis that replays with 0 / 2715 tx failures and boots a livegnoland-1node.Dependencies / related PRs
contribs/tx-archivemetadata +SignerInfopopulator) for replay-ready backupsAI disclosure
Developed with significant assistance from Claude Code for testing, review, and iterative fixes.