Skip to content

feat(gnoland): chain hardfork mechanism v3#5511

Merged
jaekwon merged 109 commits into
masterfrom
feat/genesis-replay-upgrade3
May 7, 2026
Merged

feat(gnoland): chain hardfork mechanism v3#5511
jaekwon merged 109 commits into
masterfrom
feat/genesis-replay-upgrade3

Conversation

@moul
Copy link
Copy Markdown
Member

@moul moul commented Apr 15, 2026

Overview

Chain hardfork mechanism for gno.land: export all state and historical transactions from the source chain, replay them during InitChain on the new chain, and start producing blocks at the halted height. Replaces the original single-OriginalChainID design from #5411 with a more flexible multi-chain model (PastChainIDs allowlist + per-tx ChainID).

History:

What's in

tm2 (consensus + SDK)

  • GenesisDoc.InitialHeight consensus starts block production at this height after InitChain; Handshaker sets state.LastBlockHeight = InitialHeight - 1.
  • BlockchainReactor, state, store, validation all updated to handle chains where InitialHeight > 1 (empty block store, non-contiguous block save, validator set / consensus params persisted at InitialHeight, etc.)
  • BaseApp.lastBlockHeight tracker (this iteration): real chain height = multistoreVersion + initialHeightOffset, with the offset persisted under mainInitialHeightKey and restored on every restart. validateHeight now enforces strict contiguity against real chain height; the previous "allow monotonic jump" branch (which permanently bypassed contiguity for InitialHeight > 1 chains) is gone.
  • BaseApp.Info guard handle calls before the multistore is loaded.
  • auth.SkipGasMeteringKey context flag that lets SetGasMeter bypass the new VM's gas meter (used for GasReplayMode="source").
  • RequestInitChain.InitialHeight new ABCI field so the app can cross-check against GnoGenesisState.InitialHeight. Amino round-trip test added.

gno.land

  • GnoGenesisState extensions:
    • PastChainIDs []string allowlist of past chain IDs valid for signature verification
    • InitialHeight int64 cross-checked against GenesisDoc.InitialHeight
    • GasReplayMode string ""/"strict" (default, new VM's gas meter) or "source" (bypass gas meter, preserve source-chain outcomes)
  • GnoTxMetadata extensions:
    • BlockHeight int64 original block height
    • ChainID string originating chain ID
    • Failed bool tx had non-zero return code on source chain (skipped during replay)
    • SignerInfo []SignerAccountInfo per-signer account metadata (address, account number, pre-tx sequence) so signatures verify correctly even if earlier txs diverged
    • GasUsed, GasWanted int64 source-chain gas (populated by tx-archive, used by replay report)
  • auth.NewAccountWithUncheckedNumber (this iteration, renamed from NewAccountWithNumber): 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.
  • validateSignerInfo preflight (this iteration): scans every SignerInfo entry across all txs at the start of loadAppState. Rejects the genesis if two different addresses claim the same account number, or if a SignerInfo claims 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 to false for backwards compat. Hardfork operators set it to true so any non-skipped tx replay failure aborts InitChain instead of letting the chain boot in a corrupted state. Skipped txs (metadata.Failed = true) do not count.
  • Genesis-mode tx sig verify with PastChainIDs[0] genesis-mode txs (no metadata or BlockHeight == 0) use the first PastChainIDs entry 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 on metadata == nil (this iteration) so migration txs (metadata != nil, BlockHeight == 0, Timestamp != 0) keep their metadata-driven ctxFn instead of being silently overwritten.
  • BaseApp.InitChain error surfacing (this iteration): when InitChainer returns ResponseInitChain.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.
  • Replay report per-tx categorization emitted via logger after InitChain: ok / ok_gas_differs / failed / skipped_failed. Exposes Outcomes() and FailedCount() for external tooling.

Hardfork tooling (contribs/gnogenesis/internal/fork/)

  • gnogenesis fork generate generate a hardfork genesis from a source chain (RPC URL, local data dir, or exported tarball).
  • gnogenesis fork test local genesis replay smoke-test.
  • --patch-realm PKGPATH=SRCDIR (repeatable) rewrite a genesis-mode addpkg tx in-place with files from SRCDIR. Lets you deliver realm upgrades as part of the fork (e.g. adding a new .gno file 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-tx inject a single migration tx at the end of the historical replay.
  • bruteForceSignerSequence resolve signer sequences during export by trying candidate values against the signature.

Bugs found and fixed during review

tm2 consensus (all fixed)

  1. Fast-sync broken with InitialHeight > 1 BlockPool started at store.Height()+1 = 1 instead of state.LastBlockHeight+1 = InitialHeight. Nodes trying to fast-sync would request non-existent blocks.
  2. Validator set / consensus params not saved at InitialHeight saveState only saved validators when nextHeight == 1. With InitialHeight > 1, LoadValidators failed and LoadConsensusParams panicked at block InitialHeight+1.
  3. ValidateBasic bypass via zeroed LastBlockID any block with LastBlockID.IsZero() could skip commit validation. Fixed: only allow skip when commit is also nil/empty.
  4. BaseApp.validateHeight permanent contiguity bypass the previous "allow monotonic jump" branch compared real block height against the multistore version. After the first commit, actual > prevHeight is 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 in lastBlockHeight (this iteration).
  5. BaseApp.InitChain masking real error when loadAppState returned 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)

  1. loadAppState returns nil even on N tx failures chain booted in a corrupted state when historical-tx replay had failures. Fixed via opt-in StrictReplay in InitChainerConfig (this iteration).
  2. Migration-tx ctxFn overwrite the genesis-mode chain-ID branch fired on any metadata.BlockHeight == 0, stomping the metadata-driven Timestamp override on migration txs. Fixed: tighten predicate to metadata == nil and compose with any prior ctxFn (this iteration).
  3. NewAccountWithNumber had no SignerInfo collision check two SignerInfo entries with the same AccountNum but different addresses, or a SignerInfo colliding with a balance-init account, would silently zero the original account's balance. Fixed: rename to NewAccountWithUncheckedNumber (forcing every call site to acknowledge the precondition) plus validateSignerInfo preflight in loadAppState (this iteration).
  4. Failed-tx ResponseDeliverTx was empty (looked like success) explicit error marker so indexers can distinguish.
  5. GnoGenesisState.InitialHeight wasn't cross-checked against GenesisDoc.InitialHeight added InitialHeight to RequestInitChain and validate in loadAppState.
  6. RequestInitChain.InitialHeight had no amino round-trip test silent registration regression would only surface during a real hardfork (this iteration).

Hardfork tooling (fixed)

  1. applyOverlay silent no-op listed scripts but didn't execute them, returned success. Fixed: returns error when scripts found but execution not implemented.
  2. JSONL serialization used encoding/json instead of amino interface types (std.Msg) lost on round-trip. Fixed: both writer and reader now use amino.
  3. verifyGenesisFile failure returned success tool could produce invalid genesis and exit 0. Fixed: failure aborts (opt out with --no-verify).
  4. Zero unit tests for bruteForceSignerSequence fixed: 10 table-driven tests.

Docs linter (side fix for green CI)

  • Skip 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)

  • RPC retry/resume (contribs/gnogenesis/internal/fork/source_rpc.go) a single transient error during tx fetch aborts everything; needs exponential backoff + checkpointing. Architectural, follow-up PR.
  • Streaming tx export full tx history is held in memory; will OOM on large chains. Needs streaming writer, follow-up PR.
  • queryAccountAtHeight silent 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:

  • 1babfe42a fix(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 at InitialHeight > 1, heights below InitialHeight never had blocks and replay errored with "block not found for height 1".
  • 5bf2fa53e fix(gnogenesis): default gas-storage params and gas_replay_mode in hardfork genesisbuildHardforkGenesis now defaults the post-fix(tm2,gnovm,gno.land): gas storage #5415 vm.params gas-storage fields from vm.DefaultParams() when the source has them all at zero, and sets gas_replay_mode = "source" when unset. Operator overrides preserved. 4 unit tests.
  • e31268467 feat(gnogenesis): add --skip-failing-genesis-txs and --skip-genesis-sig-verification flags to fork testmake smoketest now matches what production validators actually run.

End-to-end validation

The hf-glue testbed (#5486) runs make fetch && make init && make up against rpc.gno.land halt@704052 and produces a 192 MB hardfork genesis that replays with 0 / 2715 tx failures and boots a live gnoland-1 node.

Dependencies / related PRs

  • Depends on / pairs with: #5533 (contribs/tx-archive metadata + SignerInfo populator) for replay-ready backups
  • Used in: #5486 (hf-glue testbed)
  • Also fixed here: #5539 (docs-linter skip staging preemptive fix, committed here too to keep CI green)

AI disclosure

Developed with significant assistance from Claude Code for testing, review, and iterative fixes.

moul and others added 30 commits April 1, 2026 15:23
- 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
…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).
…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
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>
jaekwon and others added 2 commits May 3, 2026 21:49
- 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>
@jaekwon jaekwon force-pushed the feat/genesis-replay-upgrade3 branch from b54675b to c51dfd0 Compare May 4, 2026 05:06
@jaekwon
Copy link
Copy Markdown
Contributor

jaekwon commented May 4, 2026

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
each time, and wires the resulting valoper profiles into the hardfork ceremony so a freshly-forked chain has full
operator-keyed management of its genesis valset.

Realms

  • r/gnops/valopers — operator profiles. Register binds an operator addr (stable identity) to a signing pubkey;
    UpdateSigningKey rotates the signing key (gated on the profile's auth list, a per-op throttle from sysparams, a fee,
    and a signingRegistry uniqueness check that retains retired entries permanently). UpdateKeepRunning is the
    operator's binding opt-out signal.
  • r/sys/validators/v3 — operator-keyed valset proposal builder (NewValidatorProposalRequest over []ValoperChange),
    with create-time dedupe and single-entry Power>0 upsert (Tendermint-native semantics). valoperCache is
    push-populated from valopers (no import cycle). AssertGenesisValopersConsistent enforces "every signing addr in
    valset:current has a profile" at genesis-mode replay.

Tooling (gnogenesis fork)

  • valoper-seed — reads a CSV of (operator_addr, signing_pubkey, moniker, description, server_type) rows, validates
    per-row (canonical bech32, moniker regex mirroring the realm constant via a drift test, dedupe on canonical address,
    op_addr ≠ derive(signing_pubkey)), and emits a deterministic .jsonl of Register migration txs.
  • addpkg — emits MsgAddPackage migration txs from local package dirs (used to deploy v3 + valopers themselves into a
    fork-genesis where the source chain doesn't have them).

gnoland integration

  • InitChainer auto-runs v3.AssertGenesisValopersConsistent in hardfork mode (gated on PastChainIDs > 0 and non-empty
    req.Validators); failure is unconditionally fatal regardless of StrictReplay. Test environments without v3 deployed
    are detected and skipped with a logged warning.

Tests

  • Gno-level: proposal_test.gno covers proposal lifecycle, KR race-safety at execute time, dedupe, upsert, executor
    reads valoperCache live, no-ghost under natural rotation flow, same-block accumulation, and a documented
    phantom-state pin. cache_test.gno covers NotifyValoperChanged/RotateValoperSigningKey auth + accumulation.
    valopers_test.gno covers Register/Update flows + auth ownership.
  • Go-level: valoper_seed_test.go (CSV validation + moniker drift test against the realm constant), addpkg_test.go,
    app_test.go::TestShouldAssertValoperCoverage.
  • Integration txtar: lifecycle, power-update, removal, e2e propagation through GovDAO + EndBlocker.

Stats

33 files, +3323 / −824 lines across examples/gno.land/r/{gnops/valopers,sys/validators/v3,sys/params}/,
gno.land/pkg/gnoland/, contribs/gnogenesis/internal/fork/, and gno.land/pkg/integration/testdata/.

Dependencies

Builds on this branch's existing hardfork-replay infrastructure: PastChainIDs, --migration-tx, gnogenesis fork
generate/test, StrictReplay, --patch-realm, etc.

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>
@jaekwon jaekwon force-pushed the feat/genesis-replay-upgrade3 branch from c51dfd0 to f1c67b7 Compare May 4, 2026 05:27
jaekwon and others added 3 commits May 3, 2026 22:58
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>
@lbrown2007 lbrown2007 added the a/everyone Affects every team label May 4, 2026
moul added 6 commits May 4, 2026 21:03
- 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.
@jaekwon jaekwon self-requested a review May 7, 2026 08:06
…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.
@moul moul enabled auto-merge (squash) May 7, 2026 08:38
….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.
@moul moul disabled auto-merge May 7, 2026 14:27
@jaekwon jaekwon merged commit db1e3ec into master May 7, 2026
137 checks passed
@jaekwon jaekwon deleted the feat/genesis-replay-upgrade3 branch May 7, 2026 18:31
@github-project-automation github-project-automation Bot moved this from 📥 Inbox to ✅ Done in 😎 Manfred's Board May 7, 2026
@github-project-automation github-project-automation Bot moved this from In Progress to Done in 💪 Bounties & Worx May 7, 2026
jaekwon added a commit to jefft0/gno that referenced this pull request May 8, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a/everyone Affects every team a/gnops DevOps, Valopers, NetOps, Infra, Monitoring, Coordination team amino Issues and PRs related to amino 📖 documentation Improvements or additions to documentation 🤝 contribs 🐳 devops 🐹 golang Pull requests that update Go code 📦 🌐 tendermint v2 Issues or PRs tm2 related 📦 ⛰️ gno.land Issues or PRs gno.land package related 📦 🤖 gnovm Issues or PRs gnovm related 🧾 package/realm Tag used for new Realms or Packages.

Projects

Status: ✅ Done
Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants