Skip to content

execution/chain: emit chain.Config ChainID and TTD as unquoted JSON numbers#21282

Draft
yperbasis wants to merge 2 commits into
mainfrom
yperbasis/chain-config-json-compat
Draft

execution/chain: emit chain.Config ChainID and TTD as unquoted JSON numbers#21282
yperbasis wants to merge 2 commits into
mainfrom
yperbasis/chain-config-json-compat

Conversation

@yperbasis
Copy link
Copy Markdown
Member

Summary

  • Add Config.MarshalJSON that emits ChainID and TerminalTotalDifficulty as unquoted JSON numbers via a small legacyJSONUint256 wrapper, restoring the 3.4-compatible wire format.
  • Read paths are unchanged: uint256.Int.UnmarshalJSON already accepts both quoted and unquoted decimals.

Why

#21119 migrated chain.Config.ChainID and chain.Config.TerminalTotalDifficulty from *big.Int to *uint256.Int. uint256.Int.MarshalJSON emits a quoted decimal (\"1\"), and math/big.Int.UnmarshalJSON rejects that form. Release/3.4, which still stores these as *big.Int, therefore panics at startup on any chaindata written by 3.5+:

panic: math/big: cannot unmarshal "\"1\"" into a *big.Int

(Reproduced locally: pointing a fresh release/3.4 binary at a datadir last touched by main aborts in WriteChainConfig's read-back path before the node starts.)

Limitation

Chaindata already written by 3.5+ before this fix is not retroactively healed — it still contains the quoted-string form. Downgrades to 3.4 are only viable after a 3.5+ run with this fix re-writes the chain config.

Test plan

  • TestConfigJSONLegacyCompat pins the unquoted-number wire format and verifies the bytes decode into a *big.Int-typed struct (the 3.4 shape).
  • TestConfigJSONNilUint256Fields covers nil/omitempty.
  • make lint clean.
  • make erigon integration builds.
  • go test -short ./execution/chain/... ./db/rawdb/... ./execution/state/genesiswrite/... passes.

🤖 Generated with Claude Code

…umbers

#21119 migrated chain.Config.ChainID and chain.Config.TerminalTotalDifficulty
from *big.Int to *uint256.Int. uint256.Int.UnmarshalJSON accepts both quoted
and unquoted decimals, so Erigon reads either form. But its MarshalJSON emits
a quoted string ("1"), and math/big.Int.UnmarshalJSON rejects that form.
Release/3.4 (still on *big.Int for these fields) therefore panics at startup
on any chaindata written by 3.5+:

  panic: math/big: cannot unmarshal "\"1\"" into a *big.Int

Add a Config.MarshalJSON that emits ChainID and TerminalTotalDifficulty as
unquoted JSON numbers via a small wrapper type, restoring the 3.4-compatible
wire format. Read paths are unchanged.

Limitation: chaindata already written by 3.5+ before this fix is not retroactively
healed — it still contains the quoted-string form. Downgrades to 3.4 are only
viable after a future 3.5+ run that rewrites the config.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Restores Erigon 3.4 wire compatibility for the chain.Config JSON encoding by adding a custom MarshalJSON that emits ChainID and TerminalTotalDifficulty as unquoted JSON numbers (matching big.Int's encoding) rather than uint256.Int's default quoted-decimal string form. Without this, chaindata written by 3.5+ panics when read back by 3.4's *big.Int-typed parser.

Changes:

  • Introduce legacyJSONUint256 wrapper that marshals a *uint256.Int as an unquoted decimal (or null when nil).
  • Add Config.MarshalJSON that re-emits chainId/terminalTotalDifficulty through the wrapper via an anonymous-struct-with-embedded-alias pattern (field shadowing on JSON tags) so all other fields continue to be encoded by the default encoder.
  • Add tests pinning the unquoted-number wire format, verifying *big.Int decoding compatibility, the uint256 round-trip, and nil-field behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
execution/chain/chain_config.go Adds legacyJSONUint256 and Config.MarshalJSON to emit ChainID/TTD as unquoted decimals for 3.4 read compatibility.
execution/chain/chain_config_test.go New tests TestConfigJSONLegacyCompat and TestConfigJSONNilUint256Fields covering wire format, legacy *big.Int decode, round-trip, and nil/omitempty.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@yperbasis yperbasis marked this pull request as draft May 19, 2026 13:55
…fNotExist

The chain.Config MarshalJSON fix only takes effect when something rewrites
the config. WriteChainConfig runs unconditionally on every startup, so the
genesis-hash-keyed ConfigTable entry is healed automatically. But the
kv.GenesisKey-keyed Genesis blob is only ever written once (on first init),
so on existing chaindata it stays in the legacy quoted-string form and the
next 3.4 read still panics at ReadGenesis.

Detect the legacy form (chainId/terminalTotalDifficulty as quoted strings)
inside WriteGenesisIfNotExist and re-marshal in place. The rewrite is
one-shot: once the unquoted form is on disk, subsequent calls short-circuit.

Tested by hand-crafting a legacy blob, running WriteGenesisIfNotExist, and
asserting the stored bytes are now in big.Int-compatible form.
@yperbasis
Copy link
Copy Markdown
Member Author

Follow-up: extended the fix to also migrate the kv.GenesisKey blob in WriteGenesisIfNotExist. Without this, MarshalJSON only heals the genesis-hash-keyed ConfigTable entry (rewritten on every startup), but the kv.GenesisKey-keyed Genesis JSON stays in the legacy uint256-quoted form forever and the next 3.4 read still panics at ReadGenesis. Verified by reproducing the panic against a main-written datadir, applying the patch, and confirming 3.4 then boots cleanly.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment on lines +91 to +106
if len(existing) > 0 {
// If the stored blob predates the chain.Config JSON-compat fix, the
// ChainID / TerminalTotalDifficulty fields are quoted strings, which
// older binaries (still on *big.Int for those fields) reject. Detect
// and re-marshal in place so subsequent downgrades can read the DB.
if needsLegacyGenesisRewrite(existing) {
var stored types.Genesis
if err := json.Unmarshal(existing, &stored); err != nil {
return fmt.Errorf("rewrite legacy genesis JSON: unmarshal: %w", err)
}
val, err := json.Marshal(&stored)
if err != nil {
return fmt.Errorf("rewrite legacy genesis JSON: marshal: %w", err)
}
return db.Put(kv.ConfigTable, kv.GenesisKey, val)
}
Comment on lines +123 to +126
return bytes.Contains(blob, []byte(`"chainId":"`)) ||
bytes.Contains(blob, []byte(`"terminalTotalDifficulty":"`))
}

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