Skip to content

fix(gnoland): keep a genesis file's initial height on gnoland start - #6452

Draft
davd-gzl wants to merge 2 commits into
gnolang:masterfrom
davd-gzl:fix/streaming-genesis-initial-height
Draft

davd-gzl wants to merge 2 commits into
gnolang:masterfrom
davd-gzl:fix/streaming-genesis-initial-height

Conversation

@davd-gzl

@davd-gzl davd-gzl commented Nov 5, 2026 •

Copy link
Copy Markdown
Member
Overview

TLDR

A genesis file can ask for a chain to start above height 1, and gnoland start ignores that and starts at 1, logging one warning. This makes the node start where the file says, and stop at boot when the file's two copies of that height disagree. A chain whose file sets no height, mainnet included, sees no change.

Two copies of the height

A genesis carries its initial height twice. GenesisDoc.InitialHeight sits at the top of the file, and consensus and the block store follow it: the first block takes this height, and 0 or 1 both mean an ordinary start at 1. GnoGenesisState.InitialHeight sits inside app_state, so tooling reading the app state finds it there. gnogenesis fork generate writes both to the same value. Nothing ties them, so a hand edit can make them differ.

Two genesis loaders

Integration tests and gnodev build a GnoGenesisState in memory, and applyInMemoryAppState applies it. gnoland start cannot decode a genesis of several hundred megabytes into memory, so LoadStreamingGenesisDoc walks the file once, copies balances and transactions to an on-disk cache, and hands the app a *GenesisStateRef for applyStreamingAppState to read back.

The walk decodes top-level fields by name in decodeTopLevelField, and any name it does not list is logged as ignoring unknown top-level genesis field and skipped. The two apply functions are separate code, so a guard written for one has to be added to the other by hand.

Why the new check panics

An app refuses a genesis by setting ResponseInitChain.Error. The handshake in ReplayBlocks reads only the Go error from InitChainSync, so a returned refusal is logged and the node boots on whatever InitChain had already written. That is why enforceStrictReplay panics, and the check in applyStreamingAppState panics the same way. applyInMemoryAppState still returns its mismatch, so a mismatch there still boots. gnolang/gno#5995 proposes that the handshake read the field, after which both panics could become returns.

Before and after

Before: a genesis with both heights at 1000 starts at height 1 through gnoland start. With 1000 at the top and 7 in app_state, it also starts at 1, and nothing compares them.

After: both heights at 1000 commit the first block at 1000, with no block 1. With 1000 and 7 the node exits on panic: InitialHeight mismatch: GnoGenesisState.InitialHeight=7, GenesisDoc.InitialHeight=1000.

Words used here

Name What it is
validateInitialHeight Returns an error when the app_state height is set and differs from the top-level one. applyInMemoryAppState and applyStreamingAppState call it.
enforceStrictReplay Stops the boot with a panic when a genesis transaction failed and the operator asked for strictness.
ReplayBlocks The handshake step that sends InitChain when the app's height is 0.

4/10, medium

Problem

A hardfork genesis that sets "initial_height": "1000" starts its chain at height 1 when gnoland start reads it, and the only trace is ignoring unknown top-level genesis field. gnoland start reads genesis through the streaming loader, whose decodeTopLevelField decodes the fields it names, and initial_height is not one, so the height never reaches the handshake. #5684 added the loader after #5511 added the field.

The same path does not compare that height with app_state.initial_height, where the in-memory loader does.

Fix

gnoland start now begins the chain at the file's height and stops at boot when the two copies differ, and on a node booted that way /genesis serves the height too.

The streaming check panics rather than returns, since the handshake ignores a returned refusal: with a returned error, a genesis with 1000 at the top and 7 in app_state booted a chain at height 1000 with no accounts. Both apply functions share the comparison through validateInitialHeight.

This follows #5995, which makes the handshake read a refusal and leaves this path alone, and either can land first. Their production code merges cleanly, and whichever lands second merges two test files, app_test.go and node_initial_height_test.go.

A chain whose file sets no height sees no change: mainnet's published genesis sets neither copy, and misc/deployments holds no such file. On a file that sets one:

  • A node already running on it keeps its height across a restart.
  • A fresh node opens at the file's height rather than at 1, so it would not follow a chain that started at 1 on that file.
  • A file with only the app_state height panics on a first boot.
  • A top-level height amino cannot read, a bare number for one, stops the node at load on every boot, as any other unreadable top-level field does.

Two tests used this field as their example of an unknown one and no longer do, and TestServeGenesis_SemanticEquivalenceWithSource, which CI skips without a fixture and which passes with one carrying the field, no longer strips it.

A real gnoland start on a genesis with both heights at 1000 commits its first block at 1000, holds no block 1 and carries on across a restart, and a node restarted onto this binary from master keeps its height and app hash.

Not fixed

Design alternatives
  • How a mismatch stops the boot: the streaming loader panics, as enforceStrictReplay does. Return the error instead once the handshake reads ResponseInitChain.Error.
  • When the field is read: the loader reads it on every boot, so an unreadable height stops a restart too. Read it only on a first boot if a running chain's file must never stop its node.
How it was reviewed

Single-reader review, first: one reader checks the whole diff, its findings are fixed, and a fresh reader starts again until one finds nothing.

  • Single-reader review: 3 passes; 3 issues fixed
  • Simplify: 1 pass
  • Mutations: 11; breaking the fix on purpose turns an added test red each time
Read the code in this order

🔴 hot is the riskiest code, 🟠 warm other code, 🔵 cold a test. The tier ranks the file's risk, not how much of it changed.

  1. 🔴 hot · The check on the streaming path, gno.land/pkg/gnoland/app.go
    Reads the app_state height and stops the boot when it differs from the top-level one.
    If wrong: a genesis whose heights differ boots on no state, or a valid hardfork genesis refuses to boot.
  2. 🟠 warm · The loader reading the field, gno.land/pkg/gnoland/genesis_state_ref.go
    Fills doc.InitialHeight on the first load and on each one after it.
    If wrong: a hardfork genesis boots at height 1, or panics once the check above compares against 0.
  3. 🔵 cold · A node booted from a file, gno.land/pkg/gnoland/node_initial_height_test.go
    Boots a node from a genesis file read by the streaming loader and checks which heights the store holds.
    If wrong: a dropped height shows up only on a real hardfork boot.

gnoland start reads genesis through the streaming loader, which logged the
top-level initial_height as an unknown field and dropped it, so a hardfork
genesis started at height 1. The loader now reads it.

applyStreamingAppState had no check that app_state.initial_height agrees with
it. Both loaders now call validateInitialHeight. The streaming one panics on a
mismatch or an unreadable height, since the handshake ignores a returned
refusal and would boot a chain with no accounts until gnolang#5995 lands.
@github-actions github-actions Bot added the 📦 ⛰️ gno.land Issues or PRs gno.land package related label Nov 5, 2026
@Gno2D2

Gno2D2 commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

🛠 PR Checks Summary

All Automated Checks passed. ✅

Manual Checks (for Reviewers):
  • IGNORE the bot requirements for this PR (force green CI check)
Read More

🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers.

✅ Automated Checks (for Contributors):

🟢 Maintainers must be able to edit this pull request (more info)
🟢 Pending initial approval by a review team member, or review from tech-staff

☑️ Contributor Actions:
  1. Fix any issues flagged by automated checks.
  2. Follow the Contributor Checklist to ensure your PR is ready for review.
    • Add new tests, or document why they are unnecessary.
    • Provide clear examples/screenshots, if necessary.
    • Update documentation, if required.
    • Ensure no breaking changes, or include BREAKING CHANGE notes.
    • Link related issues/PRs, where applicable.
☑️ Reviewer Actions:
  1. Complete manual checks for the PR, including the guidelines and additional checks if applicable.
📚 Resources:
Debug
Automated Checks
Maintainers must be able to edit this pull request (more info)

If

🟢 Condition met
└── 🟢 And
    ├── 🟢 The base branch matches this pattern: ^master$
    └── 🟢 The pull request was created from a fork (head branch repo: davd-gzl/gno)

Then

🟢 Requirement satisfied
└── 🟢 Maintainer can modify this pull request

Pending initial approval by a review team member, or review from tech-staff

If

🟢 Condition met
└── 🟢 And
    ├── 🟢 The base branch matches this pattern: ^master$
    └── 🟢 Not (🔴 Pull request author is a member of the team: tech-staff)

Then

🟢 Requirement satisfied
└── 🟢 If
    ├── 🟢 Condition
    │   └── 🟢 Or
    │       ├── 🔴 At least one of these user(s) reviewed the pull request: [aronpark1007 davd-gzl jefft0 notJoon omarsy MikaelVallenet] (with state "APPROVED")
    │       ├── 🔴 At least 1 user(s) of the team tech-staff reviewed pull request
    │       └── 🟢 This pull request is a draft
    └── 🟢 Then
        └── 🟢 Not (🔴 This label is applied to pull request: review/triage-pending)

Manual Checks
**IGNORE** the bot requirements for this PR (force green CI check)

If

🟢 Condition met
└── 🟢 On every pull request

Can be checked by

  • Any user with comment edit permission

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📦 ⛰️ gno.land Issues or PRs gno.land package related

Projects

Development

Successfully merging this pull request may close these issues.

2 participants