Skip to content

feat(fee): chainspec floor with code-driven activation schedule#337

Merged
nekomoto911 merged 3 commits intoGalxe:mainfrom
AshinGau:fee
Apr 26, 2026
Merged

feat(fee): chainspec floor with code-driven activation schedule#337
nekomoto911 merged 3 commits intoGalxe:mainfrom
AshinGau:fee

Conversation

@AshinGau
Copy link
Copy Markdown
Collaborator

@AshinGau AshinGau commented Apr 25, 2026

Summary

Replaces the global hardcoded 50 Gwei floor from #335 with a chainspec-field + code-driven schedule. Non-Gravity chainspecs (Ethereum mainnet history sync) keep upstream EIP-1559 semantics; Gravity chainspecs apply the floor according to a per-branch schedule.

Design

Genesis (config.gravityMinBaseFee): carries only the floor value applicable to the latest schedule segment. Its presence marks the chainspec as Gravity; absence means no floor.

Code (this branch — main): crates/chainspec/src/constants.rs::GRAVITY_MIN_BASE_FEE_ACTIVATION_BLOCK = 0, with a single-segment schedule [0, ∞) returning the genesis value.

Trait helper: EthChainSpec::gravity_min_base_fee_at_block(block) -> Option<u64> — defaults to None, ChainSpec impl encodes the schedule. Used by both next_block_base_fee (consensus / pipe-exec block production) and EthereumPoolBuilder::build_pool (pool admission floor, queried at head + 1 and combined with the operator CLI override via max()).

Compatibility

Scenario Behavior
Ethereum mainnet history sync Genesis lacks gravityMinBaseFee → schedule returns None for any block → upstream EIP-1559, no clamp, pool admission floor = MIN_PROTOCOL_BASE_FEE (7 wei). Fixes #335 breaking the Ethereum mainnet London transition (block 12,965,000) where actual base_fee = 1 Gwei.
Gravity main Genesis sets gravityMinBaseFee = 50_000_000_000, activation block = 0 → floor enforced from genesis. Pipe-exec and pool admission both clamp at 50 Gwei.
Released testnet branch Cherry-pick this commit and override GRAVITY_MIN_BASE_FEE_ACTIVATION_BLOCK with the rolling-upgrade activation height N. Before block N the schedule returns None (matches pre-floor behavior, rolling-upgrade safe); from block N onward it returns the genesis value.

Future upgrade method

When a future release changes the floor from value X to Y at block N:

  1. Update genesis JSON: gravityMinBaseFee = Y
  2. Extend the schedule helper with a new historical segment:
fn gravity_min_base_fee_at_block(&self, block: u64) -> Option<u64> {
    if block >= GRAVITY_MIN_BASE_FEE_ACT_BLOCK_N {
        self.gravity_min_base_fee   // genesis value (Y)
    } else if block >= GRAVITY_MIN_BASE_FEE_ACT_BLOCK_M {
        Some(50_000_000_000)        // hardcoded historical X
    } else {
        None
    }
}

Historical floor values are hardcoded in code, so a node restarted across multiple hardforks always validates older blocks correctly from the binary's full schedule. Genesis-only designs (which carry only the current state) cannot guarantee this.

Drive-by

OpChainSpec was missing the gravity_hardforks impl introduced in #309 — a pre-existing bug that blocked workspace compile. Added forwarding impl for gravity_hardforks and gravity_min_base_fee_at_block.

AshinGau and others added 3 commits April 25, 2026 21:10
Replaces commit 7d0483e's global hardcoded 50 Gwei floor with a
schedule-driven approach where:

- Genesis JSON `config.gravityMinBaseFee` carries only the **latest**
  segment's floor value. Its presence marks the chainspec as Gravity;
  non-Gravity chainspecs (Ethereum mainnet during reth history sync)
  omit it and keep upstream EIP-1559 semantics.
- Activation block(s) and any historical-segment floor values live in
  branch-specific code constants. main encodes a single segment
  `[GRAVITY_MIN_BASE_FEE_ACTIVATION_BLOCK = 0, ∞)` returning the genesis
  value. Released testnet branches override the constant; future
  step-up upgrades extend the schedule with hardcoded historical
  segments. This way a node restarted across multiple hardforks always
  has the full historical schedule baked into its binary, which the
  earlier genesis-only designs could not guarantee.

Reverts the broken parts of commit 7d0483e:
- crates/consensus/common/src/validation.rs: London-transition initial
  base fee back to INITIAL_BASE_FEE. This function runs in
  EthBeaconConsensus.validate_header_against_parent — the path used by
  P2P headers downloader during history sync; the hardcoded 50 Gwei
  broke validation of the actual Ethereum mainnet London transition
  (block 12,965,000) where base_fee = 1 Gwei.
- crates/ethereum/evm/src/lib.rs: same revert at the London-fork-
  boundary basefee assignment in next_evm_env.
- crates/chainspec/src/spec.rs: make_genesis_header and initial_base_fee
  use INITIAL_BASE_FEE as the genesis fallback again. DEV chain test
  hashes revert to upstream values.
- crates/node/core/src/args/txpool.rs: TxPoolArgs default for
  --txpool.minimal-protocol-fee back to MIN_PROTOCOL_BASE_FEE (7 wei).
- crates/transaction-pool/src/test_utils/tx_gen.rs and config.rs: revert
  Gravity-specific tweaks that were workarounds for the global default.
- crates/e2e-test-utils/src/transaction.rs and
  crates/ethereum/node/tests/e2e/dev.rs: revert tx fee bumps and the
  custom_chain genesis tweak; no longer needed once the production
  pool default is back to 7 wei.

Adds the new design:
- ChainSpec.gravity_min_base_fee: Option<u64>, parsed from genesis JSON
  `config.gravityMinBaseFee`.
- crates/chainspec/src/constants.rs:
  GRAVITY_MIN_BASE_FEE_ACTIVATION_BLOCK = 0 (main schedule).
- EthChainSpec::gravity_min_base_fee_at_block(block) -> Option<u64>
  trait method, default returns None (safe for non-Gravity chainspecs).
  ChainSpec impl encodes main's single-segment schedule.
- next_block_base_fee uses the helper: when active for the next block,
  EIP-1559 result is clamped at the floor and the floor is the parent
  fallback for pre-London headers.
- crates/ethereum/node/src/node.rs EthereumPoolBuilder::build_pool:
  raises pool admission floor with .max(...) when the schedule is
  active for `head + 1`. Static decision at startup; missing the
  tighten across activation only causes sub-floor txs to sit unincluded
  (pipe-exec rejects them at production), no consensus impact.
- crates/optimism/chainspec/src/lib.rs: forward gravity_hardforks
  (pre-existing missing impl from commit d0666f2, unrelated to fee
  work but blocks workspace compile) and gravity_min_base_fee_at_block
  to inner ChainSpec.

Behavior matrix:
- Ethereum mainnet history sync: gravity_min_base_fee = None ->
  schedule helper returns None for any block -> upstream EIP-1559 with
  no clamp; upstream pool admission floor (7 wei).
- Gravity main: chainspec genesis sets gravityMinBaseFee = 50e9, top-
  level baseFeePerGas = 50e9. Schedule activates at block 0, so floor
  is enforced for every block in pipe-exec block production and pool
  admission.
- Released testnet branches: override
  GRAVITY_MIN_BASE_FEE_ACTIVATION_BLOCK with the rolling-upgrade
  activation height; before the activation block, schedule returns
  None (matches v1.4 behavior).

Verified: cargo test on reth-chainspec / reth-transaction-pool /
reth-ethereum-consensus / reth-evm-ethereum all pass; cargo check on
the relevant downstream crates clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`GRAVITY_MIN_BASE_FEE_ACTIVATION_BLOCK == 0` on main makes
`block >= 0` trivially true, which clippy::absurd_extreme_comparisons
rejects. Keep the `>=` form for branch parity (released testnet
branches override the constant to a non-zero block, future schedule
extensions add more segments) and silence the lint with a local
allow + comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nspec field

Replaces the `GRAVITY_MIN_BASE_FEE_ACTIVATION_BLOCK` code constant with
a new `ChainSpec.gravity_min_base_fee_activation_block: u64` field,
populated by `From<Genesis>` per branch:

- main: hardcoded to `0` in `From<Genesis>` (floor active from genesis,
  not configurable per genesis JSON).
- Released testnet branches: read from genesis `config.extra_fields`
  (e.g. `epsilonBlock`), reusing the same mechanism Alpha/Beta/Gamma/
  Delta already use to read their activation blocks. This lets ops
  set the rolling-upgrade activation height per-network without code
  changes.

Schedule helper `EthChainSpec::gravity_min_base_fee_at_block` now
queries the chainspec field instead of the constant, which also removes
the `clippy::absurd_extreme_comparisons` issue from Galxe#337 (no more
`block >= 0u64` literal comparison).

Historical-segment floor values from prior schedule steps still live in
branch-specific code, so nodes restarted across multiple hardforks
validate older blocks correctly from the binary's full schedule —
unchanged from the previous commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nekomoto911 nekomoto911 merged commit 364b851 into Galxe:main Apr 26, 2026
30 checks passed
ByteYue added a commit that referenced this pull request Apr 26, 2026
…torage patches

Zeta ships the v1.4 → v1.5 contract changes and the matching storage-side
bootstrapping that the new Solidity code requires.

Bytecode upgrades (ZETA_SYSTEM_UPGRADES + ZETA_EXTRA_UPGRADES):
  - Governance           (PR #83): initialize(address) + _initialized slot
  - StakingConfig        (PR #85): per-field setMinimumStakeForNextEpoch /
                                    setLockupDurationForNextEpoch /
                                    setUnbondingDelayForNextEpoch setters
  - ValidatorManagement  (PR #85): per-pool whitelist + permissionless-join flag
  - Reconfiguration      (PR #82): applyPendingConfig before DKG snapshot
  - JWKManager           (PR #79): non-empty JWK field validation on setPatches
  - StakePool            (PR #73): 2-step timelock for staker/operator/voter
                                    role changes (Gamma template is re-used;
                                    StakePool has no new immutables so the
                                    per-pool immutable patch still applies)

Storage patches (ZETA_STORAGE_PATCHES):
  - Governance slot 0 (_owner)        ← configured admin address
  - Governance slot 8 (_initialized)  ← 1 (locks out future initialize() calls)
  - ValidatorManagement._allowedPools[pool] = true for every active pool.
    _allowedPools lives at slot 7; the real per-pool slot is
    keccak256(pool_padded || slot7_padded). Those four keccak outputs are
    precomputed offline (no_std keccak can't run in const context) and
    committed as B256 literals with a regeneration hint in-source.

_permissionlessJoinEnabled (slot 8, one byte) stays at 0 intentionally —
the network launches permissioned and governance flips it later.

Base fee floor activation (#337's schedule):
  - gravity_min_base_fee_activation_block is read from the same zetaBlock
    field used for the bytecode/storage dispatch above. So the consensus-
    level 50 Gwei floor (gravity_min_base_fee_at_block) and the contract
    upgrades transition at the exact same height — one ops knob, one
    rolling-upgrade window, one logical event.
  - Defaults to u64::MAX when zetaBlock is absent, e.g. a v1.5 binary
    pointed at a v1.4 chain config that predates Zeta. This disables the
    floor schedule and falls through to upstream EIP-1559, preserving
    v1.4 base fee semantics for re-syncing historical blocks. Defaulting
    to 0 there would silently enforce the 50 Gwei floor on historical v1.4
    blocks (whose base_fee may be < 50 Gwei) and break re-sync.

Wiring:
  - crates/chainspec/src/gravity.rs       : add Zeta to GravityHardfork enum
  - crates/chainspec/src/spec.rs          : parse zetaBlock from extra_fields,
                                            and use the same value as the
                                            gravity_min_base_fee schedule
                                            activation block
  - crates/ethereum/evm/src/hardfork/mod.rs : pub mod zeta
  - crates/ethereum/evm/src/parallel_execute.rs : dispatch at Zeta block

Validated with cargo check --release on the full workspace and
cargo test on reth-chainspec / reth-pipe-exec-layer-ext-v2 gravity_hardfork_test.
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.

3 participants