Skip to content

feat(hardfork): gas-exempt system txs + zero SYSTEM_CALLER at Alpha#367

Open
nekomoto911 wants to merge 24 commits into
Galxe:mainfrom
nekomoto911:feat/system-tx-gas-exempt
Open

feat(hardfork): gas-exempt system txs + zero SYSTEM_CALLER at Alpha#367
nekomoto911 wants to merge 24 commits into
Galxe:mainfrom
nekomoto911:feat/system-tx-gas-exempt

Conversation

@nekomoto911

@nekomoto911 nekomoto911 commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Gravity Alpha hardfork makes protocol-injected system transactions (metadata onBlockStart + DKG/JWK validator txns) gas-exempt (fee-free but still gas-metered) and zeroes the historical SYSTEM_CALLER sentinel balance (~1.158×10⁵⁸ G genesis alloc) on the activation block.

Why

The SYSTEM_CALLER's genesis sentinel balance was a "fake supply" footgun:

  • Pollutes any total/circulating supply accounting.
  • Implicit dependency on the sentinel being large enough to cover system-tx base fees forever — silent invariant.
  • Concept-leakage between "protocol overhead" and "user economy".

Eliminating both the silent dependency and the fake supply in one fork closes the four Feishu-tracked issues (假供应污染 / 增发 footgun / 概念混乱 / 软不变量).

Design

Dual lever for gas exemption (load-bearing combo = L2 + disable_base_fee; disable_balance_check retained as zero-cost defensive layer):

  • L1 cfg-side (serial + grevm, byte-symmetric): disable_base_fee=true + disable_balance_check=true on transact_system_txn when Alpha is active. disable_nonce_check stays false — nonce sequence is part of the protocol contract.
  • L2 construction-side: system-tx gas_price = 0 when Alpha is active (otherwise base_fee).

One-shot balance migration at Alpha activation:

  • Routes through apply_state_change channel (same pattern as EIP-2935 boundary deployment), so serial == grevm by construction.
  • Reads SYSTEM_CALLER current AccountInfo via a new ParallelExecutor::basic accessor — hook is self-contained, no caller-side scaffolding.
  • Sets balance = U256::ZERO, preserves nonce (>0 because of per-block system txs — keeps the account non-empty under EIP-161, never pruned by state-clear) and code/code_hash (defensive — historical alloc is codeless, but the read result is ground truth).
  • Idempotency gated by transitions_at_timestamp(current_ts, parent_ts) — fires exactly on the activation block.

RPC replay parity — gas-exempt-style cfg must apply during RPC replay of post-Alpha historical blocks, otherwise once the sentinel balance hits zero those blocks would fail with insufficient funds / GasPriceLessThanBasefee. The fix is keyed on sender == SYSTEM_CALLER (recovered, not user-supplied — pure simulation endpoints are explicitly anti-spoofed).

  • Block-family endpoints (debug_traceBlock* / trace_block / trace_replayBlockTransactions): block-level EVM is rebuilt once at the system→user transaction boundary, exploiting the protocol invariant that system txs are positionally pinned at the front of the block. Invariant pinned by reth_chainspec::system_txs_form_head_prefix predicate + per-loop debug_assert!.
  • Single-tx endpoints (debug_traceTransaction / trace_transaction / trace_replayTransaction / replay_transactions_until): per-tx EVM rebuild on kind transition, with initial EVM peeked-and-pre-toggled to skip a redundant rebuild when the prelude leads with a system tx.
  • Pure simulation endpoints (eth_call / eth_estimateGas / eth_simulateV1 / debug_traceCall / trace_call* / trace_rawTransaction): unchanged — they execute user-supplied tx envs, never replay history, and user-supplied from=SYSTEM_CALLER MUST NOT receive free gas.

Single source-of-truth predicates:

  • reth_chainspec::is_system_tx_gas_exempt(chain_spec, block_ts) — the Alpha gate predicate, hoisted to reth-chainspec so RPC + execution layer + pipe layer all share one impl.
  • reth_chainspec::is_gravity_system_caller(addr) — sender comparison helper.
  • reth_chainspec::system_txs_form_head_prefix(senders) — unit-tested algorithm of record for the system-txs-at-block-head protocol invariant. Both RPC replay loops debug_assert! it.
  • reth_chainspec::SYSTEM_CALLER — the single address const.

Sibling PRs (split out for independent review)

Two pre-existing RPC↔canonical divergences originally folded into this PR have been split into their own PRs. They are orthogonal to the Alpha hardfork — neither depends on hardfork activation — and can merge in any order vs this PR:

Merge-order coordination: any of the three can land first. The other two then rebase upstream/main; git's --empty=drop auto-handles the identical patch-id commits. No manual conflict resolution needed.

Commits (23)

Group Commit
Setup / refactor 02c4d892 refactor: dedupe SYSTEM_CALLER + add gating helpers
1cbc03f5 refactor: hoist SYSTEM_CALLER read into ParallelExecutor::basic
03c37d31 refactor: hoist is_system_tx_gas_exempt helper to reth-chainspec
3411118c refactor: use is_gravity_system_caller helper at sender comparison sites
Core feature 0d50df60 feat: gas-exempt system transactions behind Alpha fork (L1+L2)
7acf27ef feat: zero SYSTEM_CALLER balance at Alpha activation (apply_state_change)
ee261914 feat(rpc): per-tx gas-exempt cfg for block-family trace endpoints
274507c5 feat(rpc): per-tx gas-exempt cfg for single-tx trace endpoints
Polish 64c41f38 refactor: drop fused_inspector machinery from GravityTracingCtx
d1107e64 perf(rpc): peek first prelude tx to seed replay EVM with matching cfg
f8487835 docs: clean up internal anchor references and stale doc paths
Tests — core invariants 31a9e5f4 test: bundle equivalence + migration hook edge cases (load-bearing)
86a068a9 test(nice): fee-zero / balance-untouched + disable_balance_check defensive verify
96eed10f test(grep): long-term regression prevention checklist
Tests — RPC 4d86493b test(e2e): Alpha-activation block + dual-backend system-tx gas-exempt parity
7826cffa test(rpc): pre-Alpha block replay regression (no false exemption)
8d73fcbf test(rpc): pure-simulation endpoints reject SYSTEM_CALLER spoofing
348c5c05 test(rpc): block-family + single-tx trace endpoints post-Alpha consistency
339c0904 test(rpc): tighten post-Alpha trace endpoints to byte-equal canonical (HP-1)
CI hygiene f0afa12f refactor(ci): move grep invariants from Rust test to bash script + CI step
ed12dedb fix(ci): backtick SYSTEM_CALLER in is_system_tx_gas_exempt doc
97728ad7 fix(ci): drop tx.clone() in Trace::trace_block_with replay loop
Invariant pin b8fe95a7 test(rpc): pin system-txs-at-block-head invariant for trace replay loops

Test plan

  • Bundle byte-equivalence between serial (WrapExecutor) and grevm (GrevmExecutor) on transact_system_txn (crates/ethereum/evm/src/parallel_execute.rs U-6) — load-bearing serial==grevm invariant.
  • Migration hook edge cases (crates/pipe-exec-layer-ext-v2/execute/src/system_caller_migration.rs#tests): activation-block balance zeroing + nonce/code preserve + EIP-161 non-pruning + idempotency + pre/post-activation no-op + degenerate fixture handling.
  • Fee-zero / balance-untouched (U-7) + gas_used equivalent pre/post-fork (U-7b) + disable_balance_check zero collateral (U-8).
  • E2E: Alpha-activation block T-1 / T / T+1 behavior under both backends, plus dual-backend state-root parity across the boundary (gravity_system_tx_gas_exempt_test.rs).
  • RPC replay: pre-Alpha block trace endpoints behave identically pre/post change (gravity_system_tx_pre_alpha_replay_test.rs).
  • RPC anti-spoof: simulation endpoints reject from=SYSTEM_CALLER free-gas attempt (gravity_system_tx_simulation_anti_spoof_test.rs).
  • RPC post-Alpha trace consistency: block-family + single-tx trace endpoints byte-equal canonical execution for blocks containing system txs (gravity_system_tx_post_alpha_trace_test.rs).
  • Invariant pin — system_txs_form_head_prefix: 7 unit tests in crates/chainspec/src/gravity.rs#tests covering empty / user-only / system-only / normal / epoch-switch shapes + two violation shapes (system-after-user / user-first-then-system).
  • Long-term regression grep checklist (scripts/check-gravity-invariants.sh, wired into the integration workflow): system-tx callsites count, SYSTEM_CALLER address single-definition, RPC historical-state gating, execute_history_block caller audit, single-predicate referenced by all layers.

Compatibility notes

  • Pre-Alpha blocks: byte-identical behavior — gas-exempt gating returns false for block_ts < alphaTime. RPC replay regression test pins this.
  • SYSTEM_CALLER balance reads: searched, no in-protocol consumer; off-chain consumers (indexers, supply-stats) reading this address should drop any special-case once Alpha activates.
  • Off-chain implications: post-Alpha system txs continue to appear in block bodies with gas_used > 0 but receipt.gas_price = 0 and no balance delta — explorers may want to label these distinctly.

Not in this PR (deferred follow-ups)

  • A grevm-upstream parallel_apply_transitions_and_create_reverts accounting fix (reverts_size is never updated, while revm serial updates both state_size and reverts_size). Discovered by U-6 bundle equivalence test; non-consensus (state root unaffected, only BundleState::size_hint() memory hint differs). Patch + cover letter prepared locally and to be sent upstream to Galxe/grevm separately. Bumping the grevm pin once landed will tighten U-6 from "structural equality minus size_hint" to full byte equality.

Branch

Built on top of upstream/main 6ea2ba328c (PR #366, randomness-by-height precompile gas guard). Backup of pre-shrink tip at feat/system-tx-gas-exempt-pre-shrink locally (29 commits, included BLS+R9 trio + their CI hygiene fixes — now extracted into #370 / #371).

@nekomoto911 nekomoto911 added enhancement New feature or request hardfork labels Jun 26, 2026
nekomoto911 added a commit to nekomoto911/gravity-reth that referenced this pull request Jun 27, 2026
… step

Why

`crates/ethereum/evm/tests/grep_checklist.rs` shelled out to `rg` from
`#[test]` functions via `Command::new("rg")`. This is an
anti-pattern in two ways:

  1. The invariants are lints, not unit tests — they don't exercise
     compiled code, they only check that source patterns hold. Living
     under `cargo test` makes them look like they verify behavior
     when they verify policy.
  2. ubuntu-latest CI runners don't ship ripgrep, so every
     `#[test]` in grep_checklist.rs panicked in `Command::new("rg")
     .output().expect(...)` with NotFound. This blocked the `test /
     ethereum` job on PR Galxe#367, while the underlying system-tx
     gas-exempt feature itself was correct.

Replace with `scripts/check-gravity-invariants.sh` and run it from the
`gravity-pipe-test` CI job (which already passes; ~5s ripgrep apt
install is acceptable on one job, not the matrix). Each invariant
prints OK on success or fails with a paste-able `rg` command on
stderr so a contributor can immediately reproduce locally.

Invariant count

8 total (6 carried over from the old grep_checklist.rs + 2 HP-2
additions that were missing):

  1. fn transact_system_txn lives in exactly 2 source files
     (serial + grevm impls).
  2. SYSTEM_CALLER address literal 625f0000 has a single source of
     truth in chainspec/src/gravity.rs (+ tests).
  3. trace.rs RPC replay uses historical state.
  4. execute_history_block has no non-test callers.
  5. Pipe + RPC custom precompile registration in sync.
  6. is_system_tx_gas_exempt predicate is wired in all three layers.
  7. (HP-2 new) disable_base_fee / disable_balance_check writes are
     either fork-gated (file co-references is_system_tx_gas_exempt /
     is_gravity_system_caller / GravityHardfork::Alpha /
     SYSTEM_CALLER) or in approved buckets (test files; the pure-
     simulation endpoint estimate.rs, which legitimately sets
     disable_base_fee unconditionally — gas-exempt-feature N/A
     because it doesn't replay user-signed txs).
  8. (HP-2 new) RPC replay paths
     (replay_transactions_until / trace_block_until_with_inspector)
     per-tx sender-check by referencing is_system_tx_gas_exempt or
     is_gravity_system_caller in the same file. Catches a future PR
     that adds a replay caller without wiring the SYSTEM_CALLER
     check.

Both HP-2 invariants were called out as missing in the design's
acceptance-tests §5 (Galxe#3 / Galxe#4); the original grep_checklist replaced
them with `is_system_tx_gas_exempt_referenced_by_all_three_layers`
which is strictly weaker (it checks the predicate is wired at
crate scope; the HP-2 invariants check fork-gate and replay-path
parity at file scope).

CI step placement

`gravity-pipe-test` job in `.github/workflows/integration.yml`
(already runs ubuntu-latest, already touches gravity-specific
e2e, already passes). The `test / ethereum` matrix job is
deliberately NOT extended — keeping the apt-get install on one job
saves CI time, and the matrix job's scope (full cargo nextest) is
orthogonal to a static-source lint.

Verification

  bash scripts/check-gravity-invariants.sh
  # All 8 invariants pass on HEAD as of this commit.
…g helpers

Collapse the two independent declarations of the SYSTEM_CALLER address
literal (`0x…625f0000`) — one in `pipe-exec-layer-ext-v2/.../onchain_config/mod.rs`
and one in `rpc-eth-api/src/helpers/transaction.rs` — into a single source
of truth in `reth_chainspec::gravity`. Both consumers re-export / re-import
from the new location; downstream call sites continue to use the same
`SYSTEM_CALLER` path so behavior is unchanged.

Introduce two helpers used by upcoming gas-exempt work (system-tx
gas-exempt design §3.4):
  - `reth_chainspec::is_gravity_system_caller(addr)` — defensive check.
  - `reth_evm_ethereum::is_system_tx_gas_exempt(chain_spec, block_ts)`
    — single predicate for the Alpha-active gate, shared between serial
    executor, grevm executor, pipe-layer system-tx construction and all
    RPC trace-replay paths.

Pure refactor: no behavior change, no new gating yet (helper is added
but unused — wired up in the next commits).
Wire the dual-lever gas-exempt design from system-tx gas-exempt §2.1 / §3.1 / §3.2:

L1 (cfg-side) — `EthEvmConfig::transact_system_txn` (serial) and
`GrevmExecutor::transact_system_txn` (grevm) both set
`cfg_env.disable_base_fee = true` + `cfg_env.disable_balance_check = true`
when the block timestamp is at/after Gravity Alpha activation. The two
edits are byte-identical by construction; any drift here forks state
root on system-tx blocks (PR Galxe#363 教训).

L2 (construction-side) — `execute_system_transactions` in
`pipe-exec-layer-ext-v2` now picks `gas_price = 0` for both the metadata
txn and the validator (DKG/JWK) txns when the Alpha gate is active,
keeping pre-Alpha bytes identical (still `base_fee as u128`).

`disable_nonce_check` is left `false` everywhere — SYSTEM_CALLER's nonce
sequence is part of the protocol contract.

Enables `optional_balance_check` and `optional_no_base_fee` revm features
in `reth-evm-ethereum` so the two cfg fields are accessible from the
execution layer.

Single Alpha-active predicate `is_system_tx_gas_exempt` (added in the
previous commit) routes all three callsites through one query — the
serial / grevm / pipe-layer paths share one gate.
…nge)

Add `system_caller_migration::apply_state_changes_for_block` next to the
existing EIP-2935 hook in `execute_ordered_block`. On the Alpha activation
block (gated by `transitions_at_timestamp(current_ts, parent_ts)`) the
helper:

  - reads the pre-execution SYSTEM_CALLER `AccountInfo` (now captured
    alongside the existing `initial_nonce` read so we don't pay the cost
    twice and don't need a new `ParallelExecutor::basic_ref` trait method),
  - constructs an `EvmState` diff with `balance = U256::ZERO`, **nonce
    preserved**, **code / code_hash preserved**, `status = Touched`,
  - routes the diff through `executor.apply_state_change`, which has
    symmetric serial / grevm impls, so the migration is byte-identical on
    both backends with no PR Galxe#363-class divergence risk.

The diff lands BEFORE system transactions in the same block, so the
post-execution bundle reflects the zeroed balance and the system tx
itself runs against the migrated account. With `nonce > 0` post-migration,
EIP-161 `is_empty` stays false and state-clear never prunes the account.

No effect on pre-Alpha blocks (no-op when the fork doesn't transition).
Wire the Gravity Alpha gas-exempt design through the RPC block-family
replay paths so post-Alpha trace endpoints match canonical execution
byte-for-byte once the SYSTEM_CALLER balance has been zeroed.

trace.rs (`trace_block_until_with_inspector`):
  Replace `TxTracer::try_trace_many` with a handwritten loop so we can
  flip `cfg_env.disable_base_fee` / `disable_balance_check` on the EVM at
  the system-tx → user-tx boundary (sender-keyed classification).
  Sketch A1 from the route doc — system txns are positionally pinned at
  the front of the block by `metadata_txn.rs:120/:185`, so the typical
  block requires exactly one EVM rebuild via `Evm::finish` + re-build.
  The fork gate keys off the replayed block's timestamp, so pre-Alpha
  blocks under archive replay still go through unmodified canonical cfg.

  Introduce a local `GravityTracingCtx<'_, T, E>` that mirrors
  `alloy_evm::tracing::TracingCtx`'s 5 pub fields, exposes the previously
  crate-private `fused_inspector` / `was_fused` as pub so the
  handwritten loop can construct it, and reproduces `take_inspector`
  byte-for-byte. The 4 trait fns' F bound is updated; the 6 external
  callers' closure bodies need no change (param type is inferred). See
  R-A2 verify §2 for the (a) vs (b) decision.

  Manual fuse / `skip_last_commit` semantics reproduce
  `TxTracer::try_trace_many`'s defaults: every tx except the final one
  commits its state, and the inspector resets to the fused snapshot
  unless the hook calls `take_inspector`.

debug.rs (`debug_traceBlock*`):
  Already per-tx clones `evm_env` for `trace_transaction`. We pre-compute
  `exempt_fork_active` once, then on each per-tx clone toggle the two
  disables iff the tx's recovered sender == `SYSTEM_CALLER`. No
  structural change.

Cargo.toml: enable the `optional_balance_check` revm feature in
`reth-rpc-eth-api` so `disable_balance_check` is reachable.

R-A1 PASS — `TracingInspector` is per-tx reset by design (revm-inspectors
`mod.rs:244-250`), so the inspector discarded at `finish()` is already in
a post-fuse state semantically equivalent to a fresh `inspector_setup()`
call (verify trail §1).
Round out the system-tx gas-exempt RPC coverage for the single-tx family
endpoints: `debug_traceTransaction` / `trace_transaction` /
`trace_replayTransaction` / `trace_get`. Together with the previous
commit's block-family work, this closes the "post-Alpha replay diverges
from canonical" gap end-to-end.

`replay_transactions_until` (single EVM, sequential commit of pre-target
txs): track a `current_kind_system_exempt` toggle; when the kind flips
between consecutive txs, `Evm::finish` returns `(db, env)`, we flip the
two cfg disables on the env, and re-build the EVM via `evm_with_env` +
`register_custom_precompiles`. The fork gate keys off the replayed
block's timestamp (same predicate as the canonical execution layer and
the block-family path in `trace.rs`). With system txs pinned at the
front of the block, the typical case is at most one rebuild.

`spawn_trace_transaction_in_block_with_inspector` (the wrapper used by
both `debug_traceTransaction` and `trace_transaction`): if the *target*
tx's recovered sender is SYSTEM_CALLER, clone `evm_env` and toggle the
disables before passing it to `inspect`. `inspect` itself is unchanged
— the brief explicitly chose the caller-side patch.
Previously the Alpha migration hook took the previously-read AccountInfo
as an `Option<AccountInfo>` parameter, with the read performed at the
pipe-layer call site before `state` is moved into the executor. That
leaked the hook's data-needs into the call site.

Add a `basic(&mut self, addr) -> Result<Option<AccountInfo>, _>` accessor
to `Executor` / `ParallelExecutor` so the migration hook reads
SYSTEM_CALLER itself. Drop the `prev_account` parameter, simplify the
pipe-layer call to just an `executor.basic(...)`-free invocation.

Backends:
  - `BasicBlockExecutor` forwards to `RevmDatabase::basic(&mut self.db, ...)`.
  - `GrevmExecutor` forwards to its `ParallelState::basic`.
  - `Either<A, B>` forwards via match.
  - Test stub in `execute.rs` impls `unreachable!()`.

Symmetric with `apply_state_change`: read + write irregular state both
go through the trait now, so future hooks (e.g. additional fork-boundary
migrations) don't need bespoke pre-read scaffolding at the call site.

Pipe layer still pre-reads SYSTEM_CALLER nonce before `state` is moved —
that read serves `execute_system_transactions` nonce sequencing and is
unrelated to the migration.
…ases

Implements acceptance-tests-2026-06-26 §1.1 + §1.4 (must-pass tier):

1. crates/ethereum/evm/src/parallel_execute.rs (§1.1 — "承重墙" U-6):
   - `u6_test_system_tx_gas_exempt_bundle_equivalence` drives the same
     two-system-tx sequence (metadata + validator) through
     `WrapExecutor::transact_system_txn` (serial) and
     `GrevmExecutor::transact_system_txn` (grevm) under an Alpha-active
     chainspec. Asserts byte-equality on the state-root-bearing bundle
     fields (`state`, `contracts`, `state_size`, `reverts`). Any drift
     between the two backends would fork state root on system-tx blocks
     (PR Galxe#363-class regression).
   - Documents a known non-load-bearing discrepancy: grevm's
     `parallel_apply_transitions_and_create_reverts` updates `state_size`
     but not `reverts_size`. The `reverts` content itself is identical;
     only the size-hint accounting differs, so state root is unaffected.
     Flagged in-comment for grevm-side follow-up.

2. crates/pipe-exec-layer-ext-v2/execute/src/system_caller_migration.rs
   (§1.4 — migration hook edge cases):
   - `test_migration_at_activation_block_zeros_balance_preserves_rest`
   - `test_migration_idempotent_on_reexecution`
   - `test_migration_no_op_on_post_activation_blocks`
   - `test_migration_no_op_on_pre_activation_blocks`
   - `test_migration_account_not_pruned_by_eip161`
   - `test_migration_defensive_when_system_caller_absent`
   - `test_system_caller_address_literal_matches_canonical`

   These pin: balance zeroing + nonce/code preservation on the activation
   block; gate behavior on T-1 / T / T+1 boundaries; EIP-161 non-pruning
   via nonce>0; defensive handling of an absent pre-state SYSTEM_CALLER;
   and the SYSTEM_CALLER literal sanity (defends §6.1 grep Galxe#2). Tests
   live in the same crate as the `pub(crate)` hook so the function under
   test is directly callable.

All 13 new tests pass; no existing tests were modified.

Note: §1.4's matrix placement (`system_caller_migration.rs#tests`) is
preferred over the parent task's "parallel_execute.rs" placement because
the hook is `pub(crate)` to pipe-exec-layer-ext-v2 and cannot be called
from `reth-evm-ethereum` without a cyclic dependency.
…ive verify

Implements acceptance-tests-2026-06-26 §1.2 + §1.3 (nice-to-have tier):

- u7_test_system_tx_fee_zero_balance_unchanged (§1.2)
  Verifies that under the Alpha gate, SYSTEM_CALLER's balance is preserved
  bit-for-bit across a system tx (no fee debit) and the coinbase does NOT
  receive any tip (gas_price == 0). Also asserts gas_used stays positive,
  so the lever changes accounting only — not gas metering.

- u7b_test_system_tx_gas_used_equivalent_to_pre_fork (§1.2)
  Runs the same system tx through pre-Alpha (gas_price = base_fee,
  funded SYSTEM_CALLER) and post-Alpha (gas_price = 0, zeroed
  SYSTEM_CALLER) paths and asserts gas_used is identical. Locks the L1+L2
  levers as fee-only mutations.

- u8_test_disable_balance_check_zero_collateral (§1.3)
  Asserts that under Alpha-active cfg, dropping `disable_balance_check`
  while keeping `disable_base_fee` produces a byte-identical bundle —
  proving the second flag is currently a no-op defence layer (R5 verify).
  Protects against future revm prepay-path changes that might make
  `disable_balance_check` consequential without explicit re-gating.

All 3 new tests pass; runs alongside the existing U-1..U-7 / U-6 suite.
Implements acceptance-tests-2026-06-26 §5 / design §6.1 as 6 shell-out
`rg` invariants that run inside `cargo test` so they wire into the same
CI lane as the rest of the test suite.

Tests added in crates/ethereum/evm/tests/grep_checklist.rs:

  1. grep_transact_system_txn_two_callsites_only
     Pins that `fn transact_system_txn` lives in exactly two files —
     serial (lib.rs) and grevm (parallel_execute.rs). A third occurrence
     means somebody added a system-tx execution path that bypasses the
     shared `is_system_tx_gas_exempt` gate.

  2. grep_system_caller_address_literal_single_source
     Pins that the `625f0000` literal only appears in the canonical
     definition (`chainspec/src/gravity.rs`) and approved test files.
     Redeclarations are the "address-literal drift" regression from
     design §2.5.

  3. grep_rpc_replay_uses_historical_state
     Pins that `trace.rs` calls `state_at_block_id` / `parent_hash()`.
     If replay pivots to `latest` / node tip, the gate논증 from
     design §3.5.2 collapses and pre-Alpha blocks could silently get
     gas-exempt treatment.

  4. grep_execute_history_block_no_non_test_callers
     Pins that `push_history_block` / `execute_history_block` callers
     are restricted to pipe-exec-layer src/lib.rs + tests. A non-test
     caller would upgrade R6 to consensus-critical (design §3.6).

  5. grep_pipe_rpc_precompile_registration_in_sync
     Pins that both pipe layer (`custom_precompiles_for_ordered_block`)
     and RPC layer (`register_custom_precompiles`) register custom
     precompiles. If either disappears, sketch A1's EVM-rebuild path
     loses BLS / randomness on replay.

  6. grep_is_system_tx_gas_exempt_referenced_by_all_three_layers
     Pins that the shared predicate is referenced from evm, pipe, AND
     rpc crates — defends the "single source of truth" claim from
     design §3.4.

All 6 pass against the current branch state. Helper `rg_lines` shells
out to `rg --type rust` from the workspace root resolved via
`CARGO_MANIFEST_DIR` (no env var assumptions).
The Gravity-Alpha gas-exempt predicate previously lived in
`reth-evm-ethereum::is_system_tx_gas_exempt`, but `reth-rpc-eth-api`
does not depend on `reth-evm-ethereum` — so the five RPC replay sites
(block-family + single-tx-target loops in `helpers/trace.rs`,
`replay_transactions_until` in `helpers/call.rs`, `debug_traceBlock*`
per-tx loop in `rpc/src/debug.rs`, and `register_custom_precompiles`
in `rpc/src/eth/helpers/call.rs`) all reimplemented the gate inline:

    chain_spec.gravity_hardforks().is_fork_active_at_timestamp(
        GravityHardfork::Alpha, block_ts,
    )

That violates the "single predicate" promise from the system-tx
gas-exempt design (§3.4) and defeats the long-term grep checklist —
any tweak to the Alpha gate semantics (e.g. moving to a timestamp+block
dual-gate) would have to be hand-replayed across six call sites.

Move the helper to `reth-chainspec::gravity` next to `SYSTEM_CALLER` /
`is_gravity_system_caller`. Every consumer crate (reth-evm-ethereum
serial + grevm twins, reth-pipe-exec-layer-ext-v2 system-tx pricing,
both RPC crates) already depends on `reth-chainspec`, so the predicate
is now reachable everywhere without extra crate edges. `reth-evm-ethereum`
re-exports the helper for API stability.

Touches HP-1 from the system-tx gas-exempt code review:
- crates/chainspec/src/gravity.rs   — new `is_system_tx_gas_exempt`.
- crates/chainspec/src/lib.rs        — re-export.
- crates/ethereum/evm/src/lib.rs    — drop local def; re-export from chainspec.
- crates/pipe-exec-layer-ext-v2/.../lib.rs — switch to chainspec path.
- crates/rpc/rpc-eth-api/src/helpers/{trace,call}.rs — drop inline gate.
- crates/rpc/rpc/src/debug.rs                       — drop inline gate.
- crates/rpc/rpc/src/eth/helpers/call.rs            — drop inline gate.
`reth_chainspec::is_gravity_system_caller(addr)` was introduced in the
SYSTEM_CALLER address-collapsing commit (1c52fa8) precisely to give
"is this the protocol-injected sender?" check a named identity — so a
future address-shape change (or extending the gate to a set of system
addresses) has a single migration point. But the gas-exempt RPC wiring
that followed it never called the helper; five sites all open-coded
`tx.signer() == SYSTEM_CALLER` instead, leaving the helper defined-but-
never-invoked — a half-finished state flagged as HP-2 in the system-tx
gas-exempt code review.

Wire the helper through the five remaining sender checks so the helper
has actual consumers and the design intent from §2.5 ("地址字面量收口
+ 共享 helper") is realized:
- crates/rpc/rpc-eth-api/src/helpers/trace.rs — single-tx target,
  block-family `first_kind_system_exempt`, block-family per-tx
  classification.
- crates/rpc/rpc-eth-api/src/helpers/call.rs — `replay_transactions_until`
  per-tx kind detection.
- crates/rpc/rpc/src/debug.rs — `debug_traceBlock*` per-tx classification.

`SYSTEM_CALLER` is dropped from the imports of each touched file (only
comparison sites used it); pipe-layer and EVM-internal sites that still
construct from / look up by `SYSTEM_CALLER` keep importing it directly.
… verify)

The handwritten block-family tracing loop in `trace_block_until_with_inspector`
previously mirrored alloy-evm's `TxTracer` machinery byte-for-byte: it carried
a `fused_inspector` snapshot across iterations and used `was_fused` bookkeeping
to skip a conditional fuse step when the callback had `take_inspector`-ed.

R-A1 verify (§1.5) already proved this is unnecessary for sketch A1:
- `TracingInspector` is designed for per-tx reset (revm-inspectors:src/tracing/mod.rs:244-250
  documents it explicitly, `fuse()` clears every per-tx field).
- The EVM rebuild at every system→user-segment transition already calls
  `inspector_setup()` to seed a fresh inspector — equivalent in initial state
  to `fused_inspector.clone()`.
- No closure of the 6 existing callers (trace.rs's 5 + otterscan.rs's 1) reads
  either `ctx.fused_inspector` or `ctx.was_fused` directly; both were purely
  shape-aligned mirrors of alloy-evm's private fields used only by
  `take_inspector` and the surrounding loop.

This commit drops both fields from `GravityTracingCtx` (struct shrinks to the
5 pub fields `tx / result / state / inspector / db`), rewrites
`take_inspector` to `core::mem::take(self.inspector)` (needs only
`E::Inspector: Default`, which all callers' Insp types — `TracingInspector`,
`OpcodeGasInspector`, `StorageInspector` — satisfy), and unconditionally
re-seeds the inspector slot via `inspector_setup()` after every tx.

The trait F bound changes from `Insp: Clone + InspectorFor<…>` to
`Insp: Default + InspectorFor<…>` on both `trace_block_until_with_inspector`
and `trace_block_inspector`.

No callsite changes: otterscan and trace.rs continue to call
`ctx.take_inspector()` and `ctx.inspector.*` unchanged.
`replay_transactions_until` used to always build the initial EVM with
`current_kind_system_exempt = false` (disables OFF) and rely on the in-loop
transition to rebuild via `finish()` if the first replay tx happened to be a
SYSTEM_CALLER. For Alpha-active blocks the prelude almost always leads with
SYSTEM_CALLER (metadata + validator txs are pinned to the front of the block
by the pipe layer at `metadata_txn.rs:120/:185`), so the typical
`debug_trace*` / `trace_*` single-tx replay against a user tx in the tail
paid for one wasted `finish()` + `evm_with_env` + `register_custom_precompiles`
on entry.

Mirror the block-family path in `trace.rs:422-437` (`first_kind_system_exempt`):
peek the iterator, classify the first tx's sender, and pre-toggle the initial
cfg disables. The kind-transition rebuild inside the loop still fires when a
later tx flips kind (e.g. system→user at the boundary).

`register_custom_precompiles` is still invoked exactly once after every EVM
construction — once on the initial build and once on every transition rebuild.
No call is dropped; we just save one rebuild per replay when the prelude leads
with a system tx (the common case).

Aligns code style with the block-family loop and closes review MP-4.
… parity

Adds acceptance-matrix §2.1 (fork-activation block) + §2.2 (dual-backend
state-root equivalence) as a new pipe-layer integration test
`gravity_system_tx_gas_exempt_test.rs`.

§2.1 (`test_e2e_system_tx_alpha_activation_{grevm,disable_grevm}`):
- Boots a single reth node with `gravity_hardfork.json` patched so
  `alphaTime = ALPHA_TS_BASE + 100` aligns Alpha activation with block 100.
- Drives MockConsensus through blocks 1..=105 and asserts:
  * T-1 (block 99): SYSTEM_CALLER.balance still sentinel-sized (> 10^50),
    nonce == 99 (one metadata system tx per pre-Alpha block).
  * T (block 100): balance == 0 (migration hook fired), nonce preserved
    + incremented to 100.
  * T+1..T+5: balance stays 0, nonce monotonically increases — proves the
    L1 cfg lever + L2 `gas_price=0` keep working past the activation block.
- Runs under both grevm (default) and `--gravity.disable-grevm` (serial
  `WrapExecutor`) so a single-backend regression is caught immediately.

§2.2 (`test_e2e_dual_backend_state_root_equivalence_across_alpha_boundary`):
- Re-uses the same activation runner under both backends with isolated
  datadirs and asserts the per-block state-root maps match byte-for-byte
  across {T-1, T, T+1..T+5}. The pre-Alpha entry guards against baseline
  drift (anything that diverges before the fork is a pre-existing serial
  vs grevm bug, not the Alpha bundle).

Scaffolding mirrors `gravity_eip2935_test.rs` / `gravity_bls_precompile_test.rs`:
- `gravity_alpha_chainspec(ts)` patches `alphaTime` on the embedded JSON.
- `MockConsensus + push_empty_range` reuses the established pattern.
- The shared `run_pipe_e2e_test` harness drives `CliRunner` with the
  selected backend flag.
Adds acceptance-matrix §3.3 — pre-Alpha RPC replay regression coverage as
`gravity_system_tx_pre_alpha_replay_test.rs`. Pins the load-bearing "gate is
keyed on the replayed block timestamp" invariant: for any block whose
timestamp predates `alphaTime`, the RPC replay path (trace_block /
debug_traceBlock / trace_transaction / debug_traceTransaction) must NOT
activate the cfg-side `disable_base_fee` / `disable_balance_check` levers.
The system tx must traverse the standard fee path and pay
`gas_used × base_fee` out of SYSTEM_CALLER's sentinel-sized genesis alloc
balance.

Runner pushes 10 empty blocks with `alphaTime = 9_999_999_999` (never
fires), then exercises:

Phase 1 — block-family endpoints over every pre-Alpha block:
  - `trace_api.trace_block(BlockId::Number(n))` must return Ok(Some) with
    >= 1 trace per system tx.
  - `debug_api.debug_trace_block(...)` must return Ok with every entry as
    `TraceResult::Success` (no Err variants).
  - Trace count matches canonical receipt count to within +1 (reward trace
    appended for non-Alpha blocks per `extract_reward_traces`).

Phase 2 — single-tx family on a sampled block (Galxe#5):
  - `trace_api.trace_transaction(metadata_tx_hash)` must Ok(Some).
  - `debug_api.debug_trace_transaction(...)` must Ok; payload must not
    contain `InsufficientFunds` / `GasPriceLessThanBasefee` markers.

Phase 3 — gate verification:
  - For every pushed pre-Alpha block, assert
    `reth_chainspec::is_system_tx_gas_exempt(chain_spec, header.timestamp)`
    returns `false`. Guards against accidental gate-on-tip regressions
    (design §3.5.2).

Both grevm and `--gravity.disable-grevm` backends exercised independently.

Adds `alloy-rpc-types-trace` to dev-dependencies (for the `TraceResult` /
`GethDebugTracingOptions` enums the trace + debug APIs use as I/O).

Location note: §3.3 nominally targets `crates/rpc/rpc/tests/` but reth-rpc
has no tests directory and pulling in node-builder dev-deps purely for an
RPC mock would be invasive churn. The pipe-exec-layer harness already
exposes the full RPC registry (`handle.node.rpc_registry.trace_api()` /
`.debug_api()`) — same surface, smaller blast radius, consistent with
`gravity_eip2935_test.rs` exercising RPC paths from the same harness.
Adds acceptance-matrix §3.4 — pure-simulation anti-spoof regression as
`gravity_system_tx_simulation_anti_spoof_test.rs`. Pins the design §2.5
safety invariant: the gas-exempt exemption MUST be keyed on the recovered
sender of a persisted system tx (reachable only via
`RecoveredBlock::transactions_recovered()` against empty-signature
protocol-injected txs), NEVER on a user-supplied `from = SYSTEM_CALLER`
in a simulation `TransactionRequest`.

Strategy — each of 6 endpoints is invoked twice on identical post-Alpha
state:
  1. `from = SYSTEM_CALLER` (spoof attempt)
  2. `from = SPOOF_PROBE_ADDR` (fresh zero-balance counter-factual)

The runner asserts behavioral equivalence (both Ok or both Err) via the
`assert_results_equivalent` helper. Any divergence — spoof Ok while probe
Err — proves the exemption leaked through user input.

Endpoints exercised:
  - `eth_call`
  - `eth_estimateGas`
  - `eth_simulateV1`
  - `debug_traceCall`
  - `trace_call`
  - `trace_callMany`

Setup details:
  - `alphaTime = 1` so block 1 already activates Alpha; runner pushes
    10 empty blocks so SYSTEM_CALLER.balance = 0 (post-migration).
  - Sanity assertion verifies SYSTEM_CALLER.balance is 0 before exercising
    endpoints (test would be vacuous otherwise).
  - Both grevm and `--gravity.disable-grevm` backends exercised — spoof
    surfaces are codepath-independent.

`trace_rawTransaction` is omitted (it requires a signed raw payload, not a
TransactionRequest, so the from-spoof attack vector doesn't apply — the
sender is recovered from the signature).
…tency

Adds acceptance-matrix §3.1 (block family) + §3.2 (single-tx family) — the
load-bearing claim of the PR — as `gravity_system_tx_post_alpha_trace_test.rs`.
Once Alpha is active and SYSTEM_CALLER.balance == 0, the per-tx gas-exempt
levers (cfg-side `disable_base_fee` + `disable_balance_check`) must let
the RPC replay path reproduce canonical execution without failing on
`GasPriceLessThanBasefee` / `InsufficientFunds`.

§3.1 block-family — exercised on every post-Alpha block (1..=10):
  - `trace_api.trace_block`
  - `debug_api.debug_trace_block`        (every entry asserted TraceResult::Success)
  - `trace_api.replay_block_transactions`
  - `trace_api.trace_block_opcode_gas`   (Reth extension)
  - `trace_api.trace_block_storage_access` (Reth extension)
  - `trace_api.trace_filter` over 1..=10
  Trace counts cross-validated against canonical receipts from the
  provider's `receipts_by_block`.

§3.2 single-tx family — target = metadata system tx at block 5:
  - `trace_api.trace_transaction`
  - `trace_api.replay_transaction`        (trace + state_diff types)
  - `trace_api.trace_get` index 0
  - `debug_api.debug_trace_transaction`   (payload must not contain
    `InsufficientFunds` / `GasPriceLessThanBasefee` markers)
  - `trace_api.trace_transaction_opcode_gas`

Sanity assertion at the start of the runner: SYSTEM_CALLER.balance == 0
post-migration — otherwise the gas-exempt levers wouldn't be load-bearing
and this test would devolve into a trivial happy-path check.

Both grevm and `--gravity.disable-grevm` backends exercised independently.

Coverage delta vs. spec:
  - `ots_getContractCreator` skipped — needs an actual contract-creating
    tx, which empty blocks do not produce.
  - "Target = user tx with system-tx prelude" sub-case of §3.2 NOT
    covered — injecting signed user txs into the OrderedBlock alongside
    the protocol-injected metadata system tx would expand scaffolding by
    ~250 LOC. The current test still pins the load-bearing "system-tx
    segment runs gas-exempt without fee errors" half of §3.2; the
    user-tx-with-prelude half is documented as a follow-up sibling test,
    best built once the EIP-7702 helper from
    `gravity_bls_precompile_test.rs` is generalised.

Location note: as with T4/T5, RPC-surface assertions live in the
pipe-exec-layer harness because reth-rpc has no `tests/` directory and
the harness already exposes the full RPC registry — see the location-note
section in `gravity_system_tx_pre_alpha_replay_test.rs`.
Review MP-1 + MP-2 cleanup:
- Drop "_local/drafts/..." path leaks from production code comments —
  external drafts can move or vanish; the comments should stand alone.
- Drop "§/R" / "verify §N" internal-index references that depend on
  external review/design docs to decode. Keep the substantive technical
  explanation; drop only the cryptic anchor.

No semantic / functional change.
… (HP-1)

Promotes the post-Alpha trace consistency harness from "format-shaped"
weak assertions to per-tx byte-equal canonical checks across every
block-family and single-tx trace endpoint, and pins the Feishu L122
invariant that SYSTEM_CALLER system txs must still meter gas (>0)
under the L1 cfg-side exemption.

What this commit pins

1. `canonical_baseline` (the single source of per-tx truth for every
   downstream assertion) is upgraded along three axes:
   - monotonic cumulative_gas_used is enforced with `checked_sub` +
     explicit panic instead of `saturating_sub` (which would silently
     mask a non-monotonic receipt — exactly the bug shape it's
     meant to catch);
   - every per-tx `gas_used` is unconditionally asserted `> 0` with
     the message "SYSTEM_CALLER tx must still meter gas (Feishu L122
     invariant)". This fixture is pure-system-tx, so the assertion
     is unconditional rather than `if sender == SYSTEM_CALLER`; a
     mixed fixture would gate it. This is the load-bearing
     `gas_used > 0` invariant that was missing across all helpers.
   - inline doc captures the two invariants so future helpers
     inherit them for free.

2. `trace_block_opcode_gas` per-tx assertion goes from `!is_empty()`
   to `sum > 0 && sum <= cn.gas_used`. Strict equality (`sum ==
   gas_used`) is intentionally NOT asserted: opcode-gas sum does not
   equal receipt `gas_used` byte-for-byte because the receipt
   includes the per-tx intrinsic cost (21_000 + calldata) and is net
   of refunds. So `sum < gas_used` is normal; only `sum > gas_used`
   is a real regression. The doc-comment captures this intrinsic-gap
   reasoning so a future reader doesn't tighten it into a false
   equality.

3. `debug_trace_block` tx_hash check goes from `if let Some` skip-on-
   None to strict `expect(...) + assert_eq!`. reth always populates
   tx_hash for persisted block entries; the previous conditional
   would have silently masked a canonical-drift bug if the trace
   ever started returning None.

4. `trace_filter` root-trace count goes from `>=` to `==`. Post-Paris
   has no reward traces and we already filter on
   `trace_address.is_empty()` so subcalls are excluded by
   construction; equality is the right invariant. Drift in either
   direction means trace_filter is duplicating or dropping root
   traces relative to receipts.

5. ALPHA_TIME_ALWAYS / parent-ts comment: `1687126994` → `1687223762`
   (the actual decimal value of `gravity_hardfork.json` genesis
   `0x6490fdd2`). The previous value was off by ~96k seconds; the
   logical conclusion was still correct, just the verification math
   in the comment was wrong.

Note on the opcode-sum-vs-gas-used gap

Receipt `gas_used` = intrinsic (21_000 + calldata zero/non-zero
bytes per EIP-2028) + sum(opcode costs) − refunds. Opcode-gas only
sums the middle term. For the metadata/validator system txs in
this fixture, intrinsic + calldata is typically a few hundred
thousand gas (large encoded payload), so opcode sum is materially
less than gas_used — and we'd be wrong to assert equality.

Otterscan `getContractCreator` is not covered

This fixture only produces protocol-injected metadata/validator
system txs (all `to = system contract`, no CREATE/CREATE2). The
otterscan endpoint is meaningful only for blocks containing
contract-deploying user txs; covering it would need a new fixture
mixing user CREATE txs into the OrderedBlock alongside the
system-tx prelude — out of scope for this commit.

What this commit does NOT touch

- nonce pre-seed on SYSTEM_CALLER (alloc nonce=1) is a fixture
  mirror of the production precondition that pre-Alpha system txs
  have already bumped SYSTEM_CALLER nonce before Alpha activates.
  No production semantics changed.
- The datadir persistence issue at L256 (`runner expects a fresh
  datadir`) is a T6-introduced pre-existing kink and is left for an
  independent follow-up commit.
- HP-2 (`grep_checklist.rs` portability + missing fork-gate
  invariants) is its own concern and is addressed in the follow-up
  commit on this branch.

Verification

- `cargo check --test gravity_system_tx_post_alpha_trace_test -p
  reth-pipe-exec-layer-ext-v2` clean.
- `cargo +nightly fmt --all` clean.
- Test execution itself relies on the same datadir flow as the
  prior commit; the assertion logic upgrades stand independently.
… step

Why

`crates/ethereum/evm/tests/grep_checklist.rs` shelled out to `rg` from
`#[test]` functions via `Command::new("rg")`. This is an
anti-pattern in two ways:

  1. The invariants are lints, not unit tests — they don't exercise
     compiled code, they only check that source patterns hold. Living
     under `cargo test` makes them look like they verify behavior
     when they verify policy.
  2. ubuntu-latest CI runners don't ship ripgrep, so every
     `#[test]` in grep_checklist.rs panicked in `Command::new("rg")
     .output().expect(...)` with NotFound. This blocked the `test /
     ethereum` job on PR Galxe#367, while the underlying system-tx
     gas-exempt feature itself was correct.

Replace with `scripts/check-gravity-invariants.sh` and run it from the
`gravity-pipe-test` CI job (which already passes; ~5s ripgrep apt
install is acceptable on one job, not the matrix). Each invariant
prints OK on success or fails with a paste-able `rg` command on
stderr so a contributor can immediately reproduce locally.

Invariant count

8 total (6 carried over from the old grep_checklist.rs + 2 HP-2
additions that were missing):

  1. fn transact_system_txn lives in exactly 2 source files
     (serial + grevm impls).
  2. SYSTEM_CALLER address literal 625f0000 has a single source of
     truth in chainspec/src/gravity.rs (+ tests).
  3. trace.rs RPC replay uses historical state.
  4. execute_history_block has no non-test callers.
  5. Pipe + RPC custom precompile registration in sync.
  6. is_system_tx_gas_exempt predicate is wired in all three layers.
  7. (HP-2 new) disable_base_fee / disable_balance_check writes are
     either fork-gated (file co-references is_system_tx_gas_exempt /
     is_gravity_system_caller / GravityHardfork::Alpha /
     SYSTEM_CALLER) or in approved buckets (test files; the pure-
     simulation endpoint estimate.rs, which legitimately sets
     disable_base_fee unconditionally — gas-exempt-feature N/A
     because it doesn't replay user-signed txs).
  8. (HP-2 new) RPC replay paths
     (replay_transactions_until / trace_block_until_with_inspector)
     per-tx sender-check by referencing is_system_tx_gas_exempt or
     is_gravity_system_caller in the same file. Catches a future PR
     that adds a replay caller without wiring the SYSTEM_CALLER
     check.

Both HP-2 invariants were called out as missing in the design's
acceptance-tests §5 (Galxe#3 / Galxe#4); the original grep_checklist replaced
them with `is_system_tx_gas_exempt_referenced_by_all_three_layers`
which is strictly weaker (it checks the predicate is wired at
crate scope; the HP-2 invariants check fork-gate and replay-path
parity at file scope).

CI step placement

`gravity-pipe-test` job in `.github/workflows/integration.yml`
(already runs ubuntu-latest, already touches gravity-specific
e2e, already passes). The `test / ethereum` matrix job is
deliberately NOT extended — keeping the apt-get install on one job
saves CI time, and the matrix job's scope (full cargo nextest) is
orthogonal to a static-source lint.

Verification

  bash scripts/check-gravity-invariants.sh
  # All 8 invariants pass on HEAD as of this commit.
clippy::doc-markdown flagged the bare `SYSTEM_CALLER` ident in the
doc-comment bullet of `crates/chainspec/src/gravity.rs:43`. Under
`-D warnings` (the lint job's RUSTFLAGS) this fails the workspace
clippy check.

Wrap as intra-doc link `[`SYSTEM_CALLER`]` (the constant lives in the
same module, so the link resolves) — matches the existing style on
line 30 (`[\`SYSTEM_CALLER\`]`).
`clippy::clone-on-copy` (`-D warnings`) flagged `tx.clone()` in the
`current_evm.transact(tx.clone())` site introduced by the per-tx
gas-exempt cfg refactor. `Recovered<&SignedTx>` is `Copy` (the inner type
is a reference), so `transact(tx)` auto-copies and `tx` remains usable
for the later `GravityTracingCtx { tx, .. }`.

No behavior change.
Both RPC replay paths added in this PR build a cfg-rebuild optimization on
the protocol invariant that SYSTEM_CALLER-signed transactions occupy a
contiguous head prefix of every block:

- block-family `trace_block_until_with_inspector` (`trace.rs`) — "at most
  one EVM rebuild on the system→user boundary" via peek-and-pre-toggle of
  the initial cfg + per-tx classification
- single-tx `replay_transactions_until` (`call.rs`) — same pattern for the
  pre-target replay prelude

The invariant is upheld upstream by the pipe layer (`metadata_txn.rs:120` /
`:185`, system txs inserted at positions 0..k of the block body) and by
the practical impossibility of forging a SYSTEM_CALLER signature. A
violation would silently degrade each loop's "at most one rebuild" claim
to a multi-rebuild slow path in release (still correct: the loops rebuild
unconditionally on kind transition) and almost certainly indicate a
pipe-layer regression worth catching loudly.

Three artifacts:

1. **`reth_chainspec::system_txs_form_head_prefix(senders) -> Result<(),
   usize>`** — unit-tested algorithm of record for the predicate. Lives
   next to `is_gravity_system_caller` / `SYSTEM_CALLER` since it operates
   purely on sender addresses; no extra crate edges.
2. **Inline `debug_assert!` in each replay loop** — tracks
   `saw_non_system_caller_tx: bool` per-tx; panics in debug builds with
   `(idx, block_number)` context if a SYSTEM_CALLER tx surfaces after a
   non-system tx. Release builds compile out the check and the bool.
3. **Seven unit tests** in `crates/chainspec/src/gravity.rs#tests`
   exercising all block shapes the user feedback called out:
   - empty iter (`empty_block_holds`)
   - user-only block (`user_only_block_holds`)
   - system-only block — epoch-transition shape with no user tail
     (`system_only_block_holds`)
   - normal block — 1 metadata + user tail (`normal_block_holds`)
   - epoch-switch block — metadata + N validator txs + user tail
     (`epoch_switch_block_holds`)
   - violation: system after user (`system_after_user_is_detected`)
   - violation: user first then system (`user_first_then_system_is_detected`)

Note on epoch-switch integration coverage: every existing test fixture
across the pipe layer uses `extra_data: vec![]` (no DKG/JWK validator
txs). Building a DKG/JWK fixture requires real protocol event payloads
that no current test helper produces. The unit test pins the predicate
on the epoch-switch shape (multiple consecutive SYSTEM_CALLER senders at
the head) at the algorithm level; a dedicated DKG/JWK integration fixture
is a follow-up.
@nekomoto911 nekomoto911 force-pushed the feat/system-tx-gas-exempt branch from d95f237 to b8fe95a Compare June 30, 2026 10:19
…ix doc

nightly-2026-02-01 clippy's `doc_lazy_continuation` now treats a `+`
appearing at the start of a doc-comment content line as a markdown list
marker, then flags the following 10 continuation lines as
"doc list item without indentation". Swap the lone `+` for the word
`plus` (semantically identical, naturally reads as English) so markdown
no longer parses the paragraph as a list. Pure doc-comment hygiene; no
code or test change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request hardfork

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant