Skip to content

feat(chainspec): hardcode gravity-mainnet hardfork schedule (chainId 127001)#369

Closed
nekomoto911 wants to merge 2 commits into
Galxe:mainfrom
nekomoto911:worktree-design-hardcoded-hardfork-timestamps
Closed

feat(chainspec): hardcode gravity-mainnet hardfork schedule (chainId 127001)#369
nekomoto911 wants to merge 2 commits into
Galxe:mainfrom
nekomoto911:worktree-design-hardcoded-hardfork-timestamps

Conversation

@nekomoto911

Copy link
Copy Markdown
Collaborator

Summary

Make the gravity-reth binary the authoritative source of hardfork activation for gravity mainnet (chainId 127001) by introducing two compile-time tables in crates/chainspec/src/gravity.rs:

  • GRAVITY_MAINNET_GATED_ETH_FORKS: (EthereumHardfork::Prague, Some(1_782_709_200)) — pinned to the value in the deployed mainnet genesis (gravity-mainet-gitops genesis.jsonconfig.pragueTime).
  • GRAVITY_MAINNET_GATED_GRAVITY_FORKS: (GravityHardfork::Alpha, None) — forward-defence so a stray alphaTime in mainnet genesis can't activate Alpha until governance picks a value.

For any other chain id (testnet 7771625, devnets, anything else), the override is a no-op and current genesis-driven behaviour is unchanged.

Entry semantics

  • Some(ts) → upsert the entry as ForkCondition::Timestamp(ts); ignore whatever genesis says.
  • Noneremove the matching entry from the runtime hardforks vec; ignore whatever genesis says. Removal is preferred over inserting ForkCondition::Never because Never at the tail of the canonical-ordered vec makes ChainSpec::latest_fork_id() panic — it unwraps hardforks.last() and then unwraps Option<ForkId>, which is None for Never. Functionally equivalent: ChainHardforks::fork() returns Never as the default for missing entries, and both fork_id/fork_filter already filter Never out.

Hook location

Two call sites in crates/chainspec/src/spec.rs::From<Genesis> for ChainSpec:

  • Ethereum-side: after hardforks.append(&mut time_hardforks), before the canonical-ordering pass against EthereumHardfork::mainnet().
  • Gravity-side: after the extra_fields reads (alphaTime / betaBlock / gammaBlock / deltaBlock), before ChainHardforks::new(gravity_hardforks).

This is the only production funnel for chainId 127001. The preserved ChainSpec.genesis is NOT mutated — the override is EL-local. CL/EL alignment in gravity-sdk's consensus side is out of scope for this PR.

Scope notes

  • Only timestamp-based forks are in the Gravity-side table. Beta/Gamma/Delta are block-based and would need a parallel block-keyed table; that's an explicit non-goal for v1.
  • The optimism funnel (into_optimism_chain_spec) is intentionally NOT patched (no-OP-stack policy).

Test plan

  • cargo nextest run -p reth-chainspec → 69/69 pass (19 new tests in gravity::tests)
    • Ethereum-side Prague override (with / without genesis pragueTime; testnet passthrough; Shanghai/Cancun untouched)
    • Gravity-side Alpha forward-defence (with / without genesis alphaTime; testnet passthrough)
    • fork_id / fork_filter byte-equality across genesis variants at probes [0, 99, 100, table_ts-1, table_ts, table_ts+1, u64::MAX]
    • Genesis-header end-to-end with pragueTime: 0 → override suppresses requests_hash, genesis_hash matches the "no pragueTime" variant
    • EL-local invariant: cs.genesis().config.prague_time and extra_fields.alphaTime preserved
    • From<Genesis> idempotency
    • ChainSpecBuilder::mainnet() bypass tripwire
    • latest_fork_id() no-panic regression
    • Pinned-value tripwire asserting Prague timestamp matches 1_782_709_200 (consensus-affecting; requires governance to change)
    • Direct helper unit tests for Some / None / non-mainnet no-op branches on hand-built tables (covers both EthereumHardfork and GravityHardfork via the generic helper)
  • cargo +nightly fmt --check -p reth-chainspec
  • cargo +nightly clippy -p reth-chainspec --lib --tests --no-deps -- -D warnings
  • CI

Maintenance

Adding or modifying an entry in either gated table is a mainnet consensus change. The PR doing so must reference the onchain governance / coordination record that authorised the timestamp. Existing entries must never be removed — promoting None to Some(ts) is the only allowed transition.

…127001)

Make the gravity-reth binary the authoritative source of hardfork
activation for gravity mainnet (chainId 127001). On that chain id the
two compile-time tables in `gravity::GRAVITY_MAINNET_GATED_ETH_FORKS`
and `gravity::GRAVITY_MAINNET_GATED_GRAVITY_FORKS` override whatever
the operator's genesis.json supplies; for any other chain id the
override is a no-op and genesis-driven behaviour is unchanged.

Initial table content:
- Prague → Some(1_782_709_200), the timestamp pinned in the deployed
  mainnet genesis (gravity-mainet-gitops/genesis.json -> config.pragueTime).
- Alpha  → None — forward-defence so a stray `alphaTime` in mainnet
  genesis cannot activate Alpha until governance picks a value.

Entry semantics: Some(ts) upserts as ForkCondition::Timestamp(ts);
None removes the entry from the runtime hardforks vec. Removal is
preferred over inserting ForkCondition::Never because Never at the
tail of the canonical-ordered vec makes ChainSpec::latest_fork_id()
panic (it unwraps hardforks.last() then unwraps an Option<ForkId> that
is None for Never). Functionally equivalent: ChainHardforks::fork()
returns Never as default for missing entries, and both fork_id and
fork_filter already skip Never entries.

The override is wired only into the From<Genesis> for ChainSpec funnel
inside into_ethereum_chain_spec — the only production path for chain
id 127001. ChainSpec::genesis itself is NOT mutated; CL/EL alignment
in gravity-sdk's consensus side is out of scope.

19 unit tests cover the Ethereum-side (Prague) override, the
Gravity-side (Alpha) override, the helper's Some/None branches on
hand-built tables, the EL-local preservation invariant, fork-id /
fork-filter stability across genesis variants, the genesis-header
end-to-end effect, idempotency, the ChainSpecBuilder bypass tripwire,
the latest_fork_id no-panic regression, and a pinned-value tripwire
asserting the Prague timestamp matches the deployed mainnet genesis.
…verrides

Make the hardcoded hardfork mechanism extensible to other chain ids the
binary may own in the future. Replace the gravity-mainnet-only flat
tables with chain-id-keyed dispatch maps and rename the entry points
accordingly:

  apply_gravity_mainnet_eth_overrides     → apply_hardcoded_eth_overrides
  apply_gravity_mainnet_gravity_overrides → apply_hardcoded_gravity_overrides

  GRAVITY_MAINNET_GATED_ETH_FORKS         → HARDCODED_ETH_FORK_OVERRIDES
                                            (now `[(chain_id, &[...])]`)
  GRAVITY_MAINNET_GATED_GRAVITY_FORKS     → HARDCODED_GRAVITY_FORK_OVERRIDES
                                            (now `[(chain_id, &[...])]`)

A `HardcodedOverrideMap<H>` type alias keeps clippy::type_complexity
happy without sacrificing readability. A new `lookup_overrides` helper
walks the dispatch map; the chain-id check is now external to
`apply_overrides_from_table`, which becomes a pure transform.

Adding another chain id is a single tuple inside each dispatch map —
no changes to the public surface or the spec.rs hook call sites.

Existing tests adjusted for the new signature (`apply_overrides_from_table`
no longer takes chain_id), plus two new tests for `lookup_overrides`
(found-and-not-found branches) and one renamed test for the dispatch-
miss noop on the public entry points.
@Richard1048576

Copy link
Copy Markdown
Collaborator

Follow-up (separate PR): unify the CL and EL hardfork schedules under one authority

This PR makes the EL hardfork schedule binary-authoritative for mainnet — which fixes the live pragueTime genesis drift. But the CL side has a second, independent hardfork system that this authority doesn't reach: gaptos' ConsensusHardforks, initialized in gravity-sdk's gravity_node/src/main.rs from chain_spec.genesis.config.extra_fields["consensusAlpha"]. Because this PR deliberately preserves ChainSpec.genesis unmutated, that path still reads the raw, un-overridden genesis — so it stays 100% genesis-driven with no forward-defence.

It's the same class of bug this PR just fixed for the EL, but with a larger blast radius: ConsensusAlpha gates BlockInfo's BCS serialization (7-field → 8-field), and BlockInfo feeds the BFT-signed LedgerInfo. A drift there — e.g. a genesis regen dropping consensusAlpha, exactly like the one that dropped pragueTime — is a consensus-layer split, not just an execution mismatch.

Suggested direction:

  • main.rs already holds the greth chain_spec, which is binary-authoritative after this PR. Derive ConsensusAlpha from that schedule instead of re-reading genesis extra_fields, and drop the drift-prone consensusAlpha field. One source of truth, the CL inherits this PR's authority, and no new cross-layer dependency (the chain_spec is already in hand at that call site).

To confirm before merging the layers:

  1. Activation domain — EVM forks key on block timestamp; this CL fork changes BCS serialization, whose natural cutover boundary in Aptos is the epoch. Per-timestamp activation is consensus-safe (block timestamps are BFT-agreed), but whether a BCS format switch should be epoch-aligned is a call for the gaptos/consensus owners.
  2. One fork or two? — if "Gravity Alpha" is meant to activate as a single coordinated upgrade, merge them onto one coordinate; if ConsensusAlpha is an independent format migration, unify only the mechanism (binary authority) and keep the activation points separate.

Cheapest to do now, while both alphaTime and consensusAlpha are still inactive on mainnet — no activated state to migrate. Not for this PR; flagging it as the real "CL/EL alignment" follow-up.

@Richard1048576

Copy link
Copy Markdown
Collaborator

Minor: log a warning when the hardcoded override disagrees with genesis

The override is silent when it differs from genesis (e.g. genesis pragueTime unset, but the binary forces 1782709200). Suggest emitting a startup WARN whenever apply_hardcoded_*_overrides actually changes a fork's condition versus what genesis specified — naming the fork, the genesis value, and the forced value.

Rationale: the override is a safety net, but a silent one hides the root cause. The motivating case here is a genesis regen that dropped pragueTime — with a silent override nobody notices the genesis is wrong, it just gets papered over indefinitely. A one-line log turns it into "genesis disagrees with the binary, go fix the genesis." Just a few lines in the override helper.

@nekomoto911

Copy link
Copy Markdown
Collaborator Author

Superseded by Galxe/gravity-sdk#763, which implements the same hardfork override on the sdk side (Option D-1). #763 unifies EL+CL via the parser-level override and avoids the admin_nodeInfo RPC inconsistency this PR had. Closing here in favour of that approach.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants