feat(chainspec): binary-authoritative hardfork schedule for gravity chains#763
Open
nekomoto911 wants to merge 2 commits into
Open
feat(chainspec): binary-authoritative hardfork schedule for gravity chains#763nekomoto911 wants to merge 2 commits into
nekomoto911 wants to merge 2 commits into
Conversation
…hains
Add `GravityChainSpecParser` (gravity_node/src/chainspec.rs) wrapping
upstream `EthereumChainSpecParser`. On the file / inline-JSON fallthrough
path, the parser mutates the `alloy_genesis::Genesis` struct in place
BEFORE `From<Genesis> for ChainSpec` runs in greth, so:
- The constructed `Arc<ChainSpec>` (including derived `genesis_header`,
`fork_id`, `fork_filter`) reflects the binary's authoritative values
from the moment it is built — no post-construction mutation, no
`Arc::get_mut`, no `OnceLock` timing pain.
- The CL side (gaptos `ConsensusHardforks::from_genesis_extra_fields`,
main.rs:107-118) reads from the SAME mutated `extra_fields`, so EL and
CL see one consistent view from a single source of truth. No CL-side
code change needed.
Entry semantics per gated fork:
Some(ts) → write into the genesis field, overriding any operator value
None → remove the operator-supplied value if present; fork inactive
Initial table covers only `GRAVITY_MAINNET_CHAIN_ID = 127001`:
prague_time: Some(1_782_709_200) // governance-pinned to
// gravity-mainet-gitops genesis.json
alpha_time: None // forward-defence: no ts chosen
// onchain yet; operator-supplied
// alphaTime in mainnet genesis is
// dropped before EL or CL reads it
Named chains (`mainnet/sepolia/holesky/hoodi/dev`) delegate to upstream
unchanged — none of those LazyLock statics are gravity-owned chain ids,
so delegation is safe and required for `--chain dev` etc. to keep working.
The `Cli<C>` framework was already generic over `C: ChainSpecParser` with
`EthereumChainSpecParser` only as the default — swapping to
`GravityChainSpecParser` is a one-line default-param change plus three
type-annotation swaps. Also fixes a stray hardcoded
`FnLauncher::new::<EthereumChainSpecParser, _>` inside the generic impl
that should have been `FnLauncher::new::<C, _>`.
12 unit tests cover: named-chain delegation, Prague override (supply /
omit / testnet passthrough), Alpha forward-defence (supply / omit /
testnet passthrough), post-override genesis preservation, idempotency,
ChainSpecBuilder bypass tripwire, and the consensus-affecting Prague
timestamp pin tripwire.
Discussion: this PR is the "Option D-1" alternative to greth PR Galxe#369,
which applied the override inside `From<Genesis> for ChainSpec`. D-1
moves the override to gravity-sdk (one repo) and naturally unifies CL+EL
under one table at the cost of losing greth's `From<Genesis>` chokepoint
(any greth-internal code path that constructs ChainSpec from Genesis on
chainId 127001 bypasses — today none exist in production).
This was referenced Jun 30, 2026
Contributor
Author
|
Tracked from review:
Neither is a blocker for this PR as it stands today (mainnet |
Closed
4 tasks
Address review feedback from gravity-reth Galxe#369: the override is silent when it differs from genesis, which papers over root causes like a genesis regen that drops a hardfork field. Refactor `apply_overrides` to return a `Vec<OverrideEvent>` listing every fork whose effective condition was actually changed by the override (i.e. `genesis_value != table_value`). `parse()` iterates the list and writes one `WARN [GravityChainSpecParser] ...` line per event to stderr — including the chain id, the fork name, the genesis-supplied value (or `<unset>`), and the binary-forced value (or `<unset>`). `tracing` is intentionally NOT used here: clap calls `parse()` inside `Cli::parse()`, before `main()` reaches `cli.run()` and installs the subscriber. `eprintln!` lands in stderr, which production `start.sh` redirects into `${LOG_DIR}/debug.log` — operators can grep for `WARN [GravityChainSpecParser]` to find any genesis/binary disagreement at startup. If `genesis_value == table_value` on every gated fork, the override is fully silent — no spurious noise on a correctly-configured node. Adds 7 new unit tests (19 total in `chainspec::tests`): - both forks disagree → both events fire, in declared table order - single-fork mismatch (genesis differs / genesis omits) → one event - genesis already matches table → silent - chain id not in HARDCODED → entirely silent - stderr line format pinned: `WARN [GravityChainSpecParser]` prefix, `chainId <id>`, fork name, raw values, `<unset>` for None
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Make the gravity-sdk binary the authoritative source of hardfork
activation for the chain ids it owns (today: gravity mainnet
127001). IntroducesGravityChainSpecParserinbin/gravity_node/src/chainspec.rsthat wraps upstreamEthereumChainSpecParserand applies a compile-time override table tothe
alloy_genesis::Genesisvalue on the file / inline-JSON fallthroughpath, before
From<Genesis> for ChainSpecruns in greth.Initial table on
GRAVITY_MAINNET_CHAIN_ID = 127001:prague_time: Some(1_782_709_200)— governance-pinned to the value ingravity-mainet-gitops/genesis.json.alpha_time: None— forward-defence: no timestamp has been chosen onchain yet; operator-suppliedalphaTimein mainnet genesis is dropped before EL or CL reads it.For any chain id not in the table, the parser is a no-op and current genesis-driven behaviour is unchanged. Named chains (
mainnet/sepolia/holesky/hoodi/dev) delegate to upstream unchanged — none of those are gravity-owned.Why mutate
Genesisbefore construction?Two big properties fall out of running the override at parse time:
Arc<ChainSpec>is correct from t=0.genesis_header,fork_id,fork_filterand the innerSealedHeader::hashOnceLockall derive from the post-overridehardforks, with no post-construction mutation, noArc::get_mut/Arc::make_mut, no recompute path.ConsensusHardforks::from_genesis_extra_fields,bin/gravity_node/src/main.rs:107-118) reads fromchain_spec.genesis.config.extra_fields. Because we mutateextra_fieldsat parse time, a singlealpha_timeentry in the table forward-defends both layers — no parallel CL table, no CL-side code change. When governance picks a timestamp for Alpha, flippingalpha_time: NonetoSome(ts)pins both layers to the same authoritative value.How does it plug in?
The
Cli<C>framework was already generic overC: ChainSpecParserwithEthereumChainSpecParseronly as the default. Swap the default + three type annotations:Also fixes a stray hardcoded
FnLauncher::new::<EthereumChainSpecParser, _>inside the generic impl that should have beenFnLauncher::new::<C, _>.Test plan
RUSTFLAGS='--cfg tokio_unstable' cargo test -p gravity_node --bin gravity_node chainspec::→ 12/12 passchain_spec.genesisreflects the table (unified-view invariant)From<Genesis>is idempotent on the same parsed JSONChainSpecBuilder::mainnet()bypass tripwire (currently safe: chain id 1, not 127001)cargo fmt -p gravity_node --checkRelationship to gravity-reth PR #369
This is the "Option D-1" alternative to Galxe/gravity-reth#369, which placed the same override inside greth's
impl From<Genesis> for ChainSpec. D-1 trades:Gain:
admin_nodeInfoRPC inconsistency in feat: use pending txn as iterator #369 (where operator-suppliedpragueTimewins over the override in the spread atcrates/rpc/rpc/src/admin.rs:114-141).Lose:
From<Genesis>is no longer the chokepoint. Any greth-internal code path that constructs aChainSpecfrom aGenesison chainId 127001 without going through the sdk parser silently skips the override. Today no such production path exists; the tripwire test covers the most likely accidental case.The two PRs are mutually exclusive — only one should land. Discussion welcome.
Maintenance
Adding or modifying an entry in the table for a shipped chain id is a consensus change. The PR doing so must reference the onchain governance / coordination record that authorised the timestamp. Existing entries must never be removed; promoting
NonetoSome(ts)is the only allowed transition.