feat(hardfork): gas-exempt system txs + zero SYSTEM_CALLER at Alpha#367
Conversation
… 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.
d95f237 to
b8fe95a
Compare
… 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.
Richard1048576
left a comment
There was a problem hiding this comment.
Approving. This cleanly addresses the earlier scope feedback — the BLS pop-verify RPC fix and the R9 provider-equivalence test are split out into #370 and #371, leaving this PR focused on the Alpha hardfork itself.
Checked that this PR stands on its own: no compile dependency on #370 (the remaining bls mentions here are test-harness comments, not symbols), and the hardfork-specific tests are all retained — gravity_system_tx_gas_exempt / pre_alpha_replay / simulation_anti_spoof / post_alpha_trace.
Design and behavior were verified end-to-end earlier:
- Gas-exempt dual lever — cfg-side
disable_base_fee(plus defensivedisable_balance_check) with construction-sidegas_price = 0, both gated on Alpha;disable_nonce_checkcorrectly leftfalse. - One-shot SYSTEM_CALLER → 0 at activation via the
apply_state_changeboundary (EIP-2935-style), preserving nonce/code. - RPC replay parity for the trace/debug endpoints.
Ran a 3-validator real-node A/B across the Alpha boundary: SYSTEM_CALLER zeroes exactly at the activation block, stays 0 with its nonce still incrementing post-Alpha, unrelated account state is byte-identical across the boundary, and debug_traceBlockByNumber on a post-Alpha system-tx block replays without an insufficient funds failure. Not retroactive on mainnet — alphaTime is unset in the deployed genesis, so this is a clean future activation. LGTM.
…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.
…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.
5aa7235 to
06ddc65
Compare
U-6 (`u6_test_system_tx_gas_exempt_bundle_equivalence`) uses `GravityHardfork::Alpha` in `alpha_active_chainspec`, but the earlier hoist-to-chainspec refactor dropped the top-of-file `GravityHardfork` import and never brought it back into the test module — CI compiled green while local nextest kept passing because the test module wasn't rebuilt in isolation. Add `GravityHardfork` to the test module's `reth_chainspec` use-list.
CI on PR Galxe#377 revealed that `gravity_system_tx_gas_exempt_test:: test_e2e_dual_backend_state_root_equivalence_across_alpha_boundary` drives `run_pipe_e2e_test` twice inside one `#[test]` fn (grevm + serial). Each call routes through `NodeCommand::execute` → `init_gravity_config`, which uses a process-global `OnceLock` guarded by `assert!(set().is_ok())`. Nextest gives each `#[test]` its own process, but two invocations inside one test share it — the second call panics with `"Global gravity config already initialized"`. The single-invocation `test_e2e_system_tx_alpha_activation_{grevm, disable_grevm}` in the same binary pass fine and stay wired. Skip only the dual-backend equivalence test until Galxe#367 follow-up splits it into two `#[test]` fns with cross-process state persistence.
…rom #367) (#370) * fix(rpc): register BLS pop-verify precompile unconditionally (gravity-audit§3.5.0) The pipe execution layer registers the BLS12-381 pop-verify precompile (0x…625f5001) on **every** block: - pre-Alpha via `pre_alpha_precompiles` (`pipe-exec-layer-ext-v2/execute/src/lib.rs:1495-1497`) - post-Alpha alongside randomness (`pipe-exec-layer-ext-v2/execute/src/lib.rs:393-396`) The RPC side, however, only registered the randomness-by-height precompile, and only when Alpha is active — BLS was never registered at all. Any `debug_traceBlock` / `debug_traceTransaction` / `trace_block` / `trace_replayTransaction` replay of a historical block (pre-Alpha **or** post-Alpha) that calls `0x…625f5001` therefore diverges from canonical (the precompile address has no code on the RPC side, so the CALL returns 0/empty and any receipt-log assertions in the user contract fail). Fix: register BLS unconditionally at the top of `register_custom_precompiles`, before the Alpha gate. Randomness registration remains Alpha-gated to match the pipe's post-Alpha branch. BLS helper + addr are re-used from `reth_pipe_exec_layer_ext_v2` (`bls_precompile::create_bls_pop_verify_precompile` / `onchain_config::BLS_PRECOMPILE_ADDR`); no cycle is introduced because `reth-pipe-exec-layer-ext-v2` depends only on `reth-rpc-eth-api`, not on `reth-rpc`. No tests in this commit — RPC replay tests for BLS require trace-mock infrastructure and are tracked separately per `_local/drafts/system-tx-gas-exempt/acceptance-tests-2026-06-26.md §5.4`. * refactor: move BLS pop-verify precompile to gravity-precompiles Hygiene refactor: BLS pop-verify precompile lived in `crates/pipe-exec-layer-ext-v2/execute/src/bls_precompile.rs` and the `BLS_PRECOMPILE_ADDR` constant in `.../onchain_config/mod.rs`. After the BLS RPC registration fix (23a5558), `reth-rpc` started using `reth-pipe-exec-layer-ext-v2` as a prod dep to reach the BLS helper — philosophically backwards since the pipe layer sits closer to consensus. `gravity-precompiles` is the existing home for chain-specific precompiles (`randomness_by_height` already lives there). Move the BLS helper to `gravity-precompiles::bls_pop_verify`, switch both pipe-exec-layer and reth-rpc to import from there. The pipe-exec-layer dep on reth-rpc is no longer needed for BLS — it drops to dev-dependency status (still needed for the R9 randomness-provider equivalence test). Zero behaviour change: the precompile implementation, gas pricing (POP_VERIFY_GAS = 110_000), and the `register_custom_precompiles` unconditional-registration semantics are preserved verbatim. * test(rpc): BLS pop-verify precompile replay byte-equal canonical (§3.5) Adds the §3.5 BLS RPC replay must-pass row from `acceptance-tests-2026-06-26.md` — the gate that has been outstanding since commit `23a55587c4` ("fix(rpc): register BLS pop-verify precompile unconditionally"), whose commit body explicitly noted "RPC replay tests for BLS … are tracked separately". Three test functions, each booting a full pipe execution layer harness and asserting byte-equal canonical via the same callTracer output that downstream consumers see: 1. `test_rpc_bls_call_replay_byte_equal_canonical` post-Alpha block-family path. Block 5 ts is Alpha-active, contains a user tx calling `0x…625f5001` (144-byte input → 32-byte 0x00..00 output, `POP_VERIFY_GAS = 110_000` flat charge). `debug_traceBlock` + callTracer and `trace_block` (parity) are exercised; the BLS frame `to`, `gas_used`, `output.len`, and absence of error are byte-equal to canonical receipt. 2. `test_rpc_bls_call_pre_alpha_replay_unchanged` Same fixture shape, but `alphaTime` is set to a far-future value so every block timestamp is strictly pre-Alpha. This is the long-term regression guard against accidentally re-gating BLS RPC registration behind Alpha — pre-fix, RPC routed BLS calls in pre-Alpha blocks to an empty account (no 110_000 gas charge), diverging from canonical for the entire pre- Alpha history. The fix is `call.rs:117-123` (`register_custom_precompiles`) where BLS is now registered *before* the Alpha gate. 3. `test_rpc_bls_call_debug_trace_transaction_byte_equal` Single-tx path. `debug_traceTransaction` is invoked on the BLS tx hash and the call frame is walked to locate the BLS-targeted frame; the top frame's `gas_used` matches canonical, the BLS frame is error-free and emits a 32-byte output. Covers the `call.rs:733-807` `replay_transactions_until` family. Coverage matrix: | dim | test_1 | test_2 | test_3 | | endpoint family | block | block | tx | | Alpha regime | post | pre | post | | byte-equal channel | gas_used, output.len, frame.to, no error | BLS fixture: 144 zero bytes. The byte-equality holds regardless of input validity — both pipe and RPC run the same handler against the same input, so any valid 144-byte buffer drives the precompile through its full code path and produces deterministic gas + output. This reuses the unit-test fixture style from `gravity_precompiles::bls_pop_verify::tests` (no fresh blst keypair generated). Re-declares `BLS_PRECOMPILE_ADDR` as a local const to avoid pulling `gravity-precompiles` into the test crate's dev-deps (sibling `gravity_bls_precompile_test.rs:73` does the same). Run: cargo nextest run -p reth-pipe-exec-layer-ext-v2 --test gravity_system_tx_bls_replay_test → 3 passed. * fix(ci): backtick PoP identifiers in bls_pop_verify doc comments `clippy::doc-markdown` (`-D warnings`) flagged eight bare `PoP` references in module/item-level doc comments of the BLS pop-verify precompile. Wrap them in backticks so they're treated as inline code, matching the existing treatment of `POP_VERIFY_GAS`, `PrecompileInput`, etc. No behavior change. * fix(ci): drop useless .into() conversion in bls_pop_verify error path `clippy::useless-conversion` (`-D warnings`) flagged the `.into()` after `format!(...)` since `PrecompileError::Other` already takes `String`. The conversion is a no-op `String -> String`. Drop it. No behavior change. * fix(ci): backtick PoP in bls_pop_verify test-mod doc comment The two CI hygiene cherry-picks (`f30bf2b37c` PoP backticks + `d6caea4d72` useless `.into()`) covered the prod doc paths of `bls_pop_verify.rs`, but a sibling doc comment inside the file's `#[cfg(test)] mod tests` ("Generate a BLS12-381 keypair and PoP for testing") still trips `clippy::doc-markdown` once the code moved from `pipe-exec-layer-ext-v2` (which had a permissive module-level allow) to `gravity-precompiles`. Backtick `PoP` to match the eight prod sites.
…iants U-6 (parallel_execute.rs:770) already proved single-tx bundle equivalence between WrapExecutor and GrevmExecutor at Alpha. §2.2's E2E dual test tried to extend that claim across three axes U-6 didn't cover: the migration hook (sentinel → 0 transition), cross-block state carryover, and pre-Alpha baseline. That E2E has been dead-on-arrival since Galxe#367 because two run_pipe_e2e_test calls inside one #[test] fn each go through NodeCommand::execute → init_gravity_config, a process-global OnceLock with a hard assert! that panics on the second call. Rather than relax the OnceLock invariant (blast radius across 15 callers of get_gravity_config) or spawn subprocesses (~80 LOC of plumbing), cover the same invariants at the unit level where no CLI runner is involved: - U-6b (system_caller_migration.rs) — calls apply_state_changes_for_block directly on both backends against seeded_db(sentinel, nonce=1) and asserts BundleState byte-equal. Proves the migration hook itself produces symmetric state. - U-6c (system_caller_migration.rs) — six-block sequence over the Alpha activation boundary. Each block: run migration hook + system tx on both backends, apply block N's bundle back into the shared ParallelState cache, assert bundle byte-equal per block. Proves cross-block carryover stays symmetric. - U-6d (parallel_execute.rs) — permanently pre-Alpha chainspec (alpha_time = u64::MAX). Runs a system tx through both backends, asserts bundle byte-equal + non-zero fee delta (proving the pre-Alpha fee path is actually exercised, not silently short-circuited). All three run against CacheDB<EmptyDB> via pure WrapExecutor / GrevmExecutor + direct migration hook invocation. No NodeCommand, CliRunner, tokio, or init_gravity_config touched. Design and adversarial review lived in the branch-private wiki; the verdict was "SAFE with caveat" — the caveat covers pipe-layer factory dispatch and payload→trie writeback, neither of which is a backend-equivalence invariant and both of which are already exercised by every other E2E test in the crate.
test_e2e_dual_backend_state_root_equivalence_across_alpha_boundary was dead-on-arrival since Galxe#367: it called run_pipe_e2e_test twice inside one #[test] fn, and each call went through NodeCommand::execute → init_gravity_config (process-global OnceLock with hard assert). The second call always panicked. The test never passed in any environment. Backend-equivalence coverage is now provided by U-6 + U-6b + U-6c + U-6d as unit tests that bypass the CLI runner entirely. See the parent commit for the coverage split. Adversarial review verdict was "SAFE with caveat": pipe-layer factory dispatch (lib.rs:313-323) and payload→trie writeback are not covered by the new unit tests, but both are already exercised by every other E2E test in the crate (gravity_pipe_test, gravity_eip2935_test, etc.), so removing this one E2E does not expose those seams. The two remaining #[test] fns in this file (test_e2e_system_tx_alpha_activation_{grevm,disable_grevm}) each call run_pipe_e2e_test exactly once and continue to pass under nextest's process-per-test isolation.
…rred #367 tests (#377) * fix(rpc): toggle gas-exempt cfg for target-tx in debug_traceTransaction `debug_trace_transaction` fetched `evm_env` via `evm_env_at`, cloned it into `replay_transactions_until` (which correctly toggles for pre-target system txs internally), then passed the *original untoggled* `evm_env` straight into `trace_transaction` for the target tx. If the target tx itself is a `SYSTEM_CALLER`-signed system tx on a post-Alpha block, the tx has `gas_price = 0` while the block's `basefee > 0` — revm rejects with `GasPriceLessThanBasefee` ("max fee per gas less than block base fee") because `disable_base_fee` was never set on the cfg. Mirror the target-tx toggle already present in `spawn_trace_transaction_in_block_with_inspector` (`crates/rpc/rpc-eth-api/src/helpers/trace.rs`) — parity/otterscan-side `trace_transaction` / `trace_replayTransaction` / `trace_get` / `trace_transaction_opcode_gas` were unaffected because they go through that helper. Only debug-namespace `debug_traceTransaction` was broken. Gate keys off the replayed block's timestamp (not node tip) so pre-Alpha historical replays remain unchanged. * ci(integration): wire deferred #367 tests + invariant 9 CI allowlist parity The gravity-pipe-test job uses an explicit `--test <name>` allowlist because the crate has integration binaries whose dev-deps are missing in this workspace (e.g. `gravity_hardfork_test.rs`). That same allowlist silently skipped every RPC replay integration test file added by #367 (and #370) — `gravity_system_tx_*_test` and `gravity_bls_precompile_test` compiled locally but never ran on CI, so a latent `debug_traceTransaction` cfg gap and #372 Track A / B gaps went unnoticed under a "✅ CI green" PR body. Wire the three tests that pass today: - gravity_system_tx_gas_exempt_test (pipe-only) - gravity_system_tx_pre_alpha_replay_test (pre-Alpha gate false) - gravity_system_tx_simulation_anti_spoof_test (simulation, no replay) Defer the two blocked ones via invariant 9's `KNOWN_UNWIRED_TESTS`: - gravity_system_tx_post_alpha_trace_test (needs #372 mint RPC reg + RPC-side Alpha migration hook; locally diverges by ~9.4k gas on block 1 metadata tx) - gravity_bls_precompile_test (needs #370 BLS RPC reg) Invariant 9 now enforces: every `gravity_system_tx_*_test.rs` / `gravity_bls_*_test.rs` file under the tests dir must be EITHER wired to CI OR listed in `KNOWN_UNWIRED_TESTS` with a documented reason. It also flags double-claimed files and stale skip-list entries whose target file no longer exists. This prevents another silent-skip incident: any new test file forces the author to make an explicit CI-wiring decision. * ci: exclude broken dual-backend test from nextest run CI on PR #377 revealed that `gravity_system_tx_gas_exempt_test:: test_e2e_dual_backend_state_root_equivalence_across_alpha_boundary` drives `run_pipe_e2e_test` twice inside one `#[test]` fn (grevm + serial). Each call routes through `NodeCommand::execute` → `init_gravity_config`, which uses a process-global `OnceLock` guarded by `assert!(set().is_ok())`. Nextest gives each `#[test]` its own process, but two invocations inside one test share it — the second call panics with `"Global gravity config already initialized"`. The single-invocation `test_e2e_system_tx_alpha_activation_{grevm, disable_grevm}` in the same binary pass fine and stay wired. Skip only the dual-backend equivalence test until #367 follow-up splits it into two `#[test]` fns with cross-process state persistence. * test: add U-6b/c/d unit tests covering §2.2 backend-equivalence invariants U-6 (parallel_execute.rs:770) already proved single-tx bundle equivalence between WrapExecutor and GrevmExecutor at Alpha. §2.2's E2E dual test tried to extend that claim across three axes U-6 didn't cover: the migration hook (sentinel → 0 transition), cross-block state carryover, and pre-Alpha baseline. That E2E has been dead-on-arrival since #367 because two run_pipe_e2e_test calls inside one #[test] fn each go through NodeCommand::execute → init_gravity_config, a process-global OnceLock with a hard assert! that panics on the second call. Rather than relax the OnceLock invariant (blast radius across 15 callers of get_gravity_config) or spawn subprocesses (~80 LOC of plumbing), cover the same invariants at the unit level where no CLI runner is involved: - U-6b (system_caller_migration.rs) — calls apply_state_changes_for_block directly on both backends against seeded_db(sentinel, nonce=1) and asserts BundleState byte-equal. Proves the migration hook itself produces symmetric state. - U-6c (system_caller_migration.rs) — six-block sequence over the Alpha activation boundary. Each block: run migration hook + system tx on both backends, apply block N's bundle back into the shared ParallelState cache, assert bundle byte-equal per block. Proves cross-block carryover stays symmetric. - U-6d (parallel_execute.rs) — permanently pre-Alpha chainspec (alpha_time = u64::MAX). Runs a system tx through both backends, asserts bundle byte-equal + non-zero fee delta (proving the pre-Alpha fee path is actually exercised, not silently short-circuited). All three run against CacheDB<EmptyDB> via pure WrapExecutor / GrevmExecutor + direct migration hook invocation. No NodeCommand, CliRunner, tokio, or init_gravity_config touched. Design and adversarial review lived in the branch-private wiki; the verdict was "SAFE with caveat" — the caveat covers pipe-layer factory dispatch and payload→trie writeback, neither of which is a backend-equivalence invariant and both of which are already exercised by every other E2E test in the crate. * test: remove §2.2 dual-backend E2E (superseded by U-6b/c/d) test_e2e_dual_backend_state_root_equivalence_across_alpha_boundary was dead-on-arrival since #367: it called run_pipe_e2e_test twice inside one #[test] fn, and each call went through NodeCommand::execute → init_gravity_config (process-global OnceLock with hard assert). The second call always panicked. The test never passed in any environment. Backend-equivalence coverage is now provided by U-6 + U-6b + U-6c + U-6d as unit tests that bypass the CLI runner entirely. See the parent commit for the coverage split. Adversarial review verdict was "SAFE with caveat": pipe-layer factory dispatch (lib.rs:313-323) and payload→trie writeback are not covered by the new unit tests, but both are already exercised by every other E2E test in the crate (gravity_pipe_test, gravity_eip2935_test, etc.), so removing this one E2E does not expose those seams. The two remaining #[test] fns in this file (test_e2e_system_tx_alpha_activation_{grevm,disable_grevm}) each call run_pipe_e2e_test exactly once and continue to pass under nextest's process-per-test isolation. * ci: drop nextest -E filter now that dual test is removed The filter workaround introduced in 01a219d excluded test_e2e_dual_backend_state_root_equivalence_across_alpha_boundary from the gravity-pipe-test job because it hit an init_gravity_config double-init panic. The test has now been removed and its backend-equivalence claim is proved by U-6/U-6b/U-6c/U-6d unit tests instead, so the filter is no longer needed. * ci: wire #370 tests to CI allowlist, unblock KNOWN_UNWIRED entry #370 (BLS pop-verify RPC registration) merged upstream during this PR, pulling in gravity_system_tx_bls_replay_test.rs and unblocking gravity_bls_precompile_test. Invariant 9 caught that #370 landed without touching the allowlist / KNOWN_UNWIRED_TESTS — exactly the silent-skip class the invariant is designed to fail on. Wire both binaries to the gravity-pipe-test workflow and drop the gravity_bls_precompile_test entry from KNOWN_UNWIRED_TESTS. * test: fix ALPHA_TIME_ALWAYS fixture in gravity_system_tx_simulation_anti_spoof_test `test_rpc_simulation_anti_spoof_grevm` and `test_rpc_simulation_anti_spoof_disable_grevm` panicked before any endpoint invocation with: [anti_spoof {label}] post-Alpha SYSTEM_CALLER.balance must be 0 left: 11579208923731619542357098500868790785326998466564056403945758400791312963993 right: 0 Root cause is the same fixture bug already documented and fixed in `gravity_system_tx_post_alpha_trace_test.rs:82-87`: `transitions_at_timestamp(parent=1687223762, current=block_n_ts, activation=1)` returns `false` for every n, because the genesis header timestamp (`0x6490fdd2 = 1687223762`) already crosses `activation = 1`. The Alpha migration hook therefore never fires and `SYSTEM_CALLER.balance` stays at the pre-migration sentinel value. Fix mirrors the pattern from `post_alpha_trace_test.rs`: 1. `ALPHA_TIME_ALWAYS` becomes `ALPHA_TS_BASE + 1` so block 1's timestamp equals `alphaTime`, and `transitions_at_timestamp` flips true exactly at block 1 (`parent = ALPHA_TS_BASE < alphaTime <= current`). 2. `gravity_alpha_chainspec` pre-seeds `SYSTEM_CALLER` with `nonce = 1` in the chainspec `alloc`, so the balance-zeroing transition produces `(balance=0, nonce=1, code=empty)` — non-empty under EIP-161 — instead of `(balance=0, nonce=0, code=empty)` which revm's AccountStatus state machine rejects with `Wrong state transition, touch empty is not possible from Loaded`. The doc-comment reference to `alphaTime = 1` in the runner is also updated to reflect the new value. Fixture-only change: no test assertion or anti-spoof logic is touched — those are precisely what the test is proving. Both `#[test]` fns now pass in ~7s each (previously panicked in ~5s). * ci: sharpen KNOWN_UNWIRED reason for gravity_system_tx_post_alpha_trace_test The previous `KNOWN_UNWIRED_TESTS` entry conflated two independent blockers ("mint precompile RPC registration + RPC-side Alpha migration hook") and gave an imprecise "~9.4k gas" divergence figure. Now that the fixture bug in `gravity_system_tx_post_alpha_trace_test.rs` is fixed (ALPHA_TIME_ALWAYS = ALPHA_TS_BASE + 1, SYSTEM_CALLER pre-seeded with nonce=1; see file header lines 82-113), the tests actually reach the trace assertions and surface the real blocker unambiguously. Empirical local run on this branch (nextest, both grevm and disable_grevm variants) fails at the same point: [post_alpha_trace grevm] trace_block(1)[0] gas_used != canonical: got 292093, want 282665 That is the block-family byte-equal trace invariant on block 1's metadata system tx, off by 9428 gas — matches the mint precompile RPC gap tracked in #372 Track A (RPC-side execution path lacks the mint precompile registration that the pipe-layer canonical path has). The RPC-side migration hook the old reason referenced is not the load- bearing blocker at this call site. Update the reason string accordingly so the next person reading the KNOWN_UNWIRED list knows exactly which fix unblocks the wiring and can verify by rerunning the test without touching any fixture. --------- Co-authored-by: Lightman <31928846+Lchangliang@users.noreply.github.com>
…onto v2.3.0 API base Merge Galxe/gravity-reth main (a379a86) into the v2.3.0 migration branch: 9 conflicted files / 18 hunks resolved, 3 auto-merged files needed revm-40 semantic fixes (ResultGas, AccountInfo::account_id, EvmOverrides signature), system_caller_migration.rs ported to the new Account/AccountInfo API. Upstream commits: Galxe#367 gas-exempt system txs + zero SYSTEM_CALLER at Alpha, Galxe#370 unconditional BLS pop-verify registration, Galxe#371 randomness equivalence tests, Galxe#374 hardfork test CI, Galxe#376 gravity-api-types bump, Galxe#377 debug_traceTransaction target-tx gas-exempt gap. Resolution ledger + shutdown-race investigation: docs/merge-v2.3.0/upstream-sync-2026-07-09.md Verified: cargo check --workspace --all-features = 0 err; fmt clean; gravity_pipe_test smoke 5/5 green (one flaky pre-existing shutdown race observed once, root-caused as reth-tasks Arc-owned-runtime teardown race, documented, not merge-introduced).
Merge upstream(Galxe)/gravity-reth-merge-v2.3.0 (22 commits, changeset static-file persistence + migrate-changesets CLI + RocksDB tuning) into the local line (upstream/main #367 sync + genesis TTD fix + persistence poll fix). Single conflict: both sides independently landed the same try_poll_persistence fix in pipe_run_inner — kept team-side comment. Verified: cargo check --workspace --all-features = 0 err.
… API base Linear replay of the Galxe main sync (a379a86): 9 conflicted files / 18 hunks resolved, 3 auto-merged files needed revm-40 semantic fixes (ResultGas, AccountInfo::account_id, EvmOverrides signature), system_caller_migration.rs ported to the new Account/AccountInfo API. Upstream commits: #367 gas-exempt system txs + zero SYSTEM_CALLER at Alpha, #370 unconditional BLS pop-verify registration, #371 randomness equivalence tests, #374 hardfork test CI, #376 gravity-api-types bump, #377 debug_traceTransaction target-tx gas-exempt gap. Resolution ledger + shutdown-race investigation: docs/merge-v2.3.0/upstream-sync-2026-07-09.md Verified: cargo check --workspace --all-features = 0 err; fmt clean; gravity_pipe_test smoke 5/5 green.
Advances the merge-base so PR #396 no longer conflicts with main. Brings in the 4 commits that landed after the 2026-07-09 sync: #383 (randomness from canonical headers), #385 (EIP-7702 cross-account nonce halt + recoverable-error panic hardening), #382 (mint precompile gas guard), #390 (withdrawal-invariant docs). The 11 conflicts are all v2.3.0/revm40 API-form divergence against the already-adapted #367-#374 batch; resolved take-ours (revm40 accessor methods, PrecompileHalt, AccountInfo::account_id, no StateCacheDbRefMutWrapper). #382's gas guard re-adapted from revm36 PrecompileError to revm40 PrecompileHalt. Verified: cargo check --workspace --all-features = 0 err; fmt clean; pipe-exec and gravity-storage --tests clean.
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:
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_checkretained as zero-cost defensive layer):disable_base_fee=true+disable_balance_check=trueontransact_system_txnwhen Alpha is active.disable_nonce_checkstaysfalse— nonce sequence is part of the protocol contract.gas_price = 0when Alpha is active (otherwisebase_fee).One-shot balance migration at Alpha activation:
apply_state_changechannel (same pattern as EIP-2935 boundary deployment), so serial == grevm by construction.AccountInfovia a newParallelExecutor::basicaccessor — hook is self-contained, no caller-side scaffolding.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).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 onsender == SYSTEM_CALLER(recovered, not user-supplied — pure simulation endpoints are explicitly anti-spoofed).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 byreth_chainspec::system_txs_form_head_prefixpredicate + per-loopdebug_assert!.debug_traceTransaction/trace_transaction/trace_replayTransaction/replay_transactions_until): per-tx EVM rebuild onkindtransition, with initial EVM peeked-and-pre-toggled to skip a redundant rebuild when the prelude leads with a system tx.eth_call/eth_estimateGas/eth_simulateV1/debug_traceCall/trace_call*/trace_rawTransaction): unchanged — they execute user-supplied tx envs, never replay history, and user-suppliedfrom=SYSTEM_CALLERMUST 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 toreth-chainspecso 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 loopsdebug_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:
fix(rpc): register BLS pop-verify precompile unconditionally (split from #367)— closes the BLS pop-verify RPC registration gap (gravity-audit §3.5.0) + hygiene refactor to move helper togravity-precompiles+ replay byte-equal-canonical test.test(rpc): equivalence of execution vs header randomness providers (split from #367)— pins byte-equality betweenExecutionRandomnessProvider(pipe) andHeaderRandomnessProvider(RPC) across 11 query-height boundaries (recent / storage / parent / future /u64::MAX).Merge-order coordination: any of the three can land first. The other two then rebase
upstream/main; git's--empty=dropauto-handles the identical patch-id commits. No manual conflict resolution needed.Commits (23)
02c4d892refactor: dedupe SYSTEM_CALLER + add gating helpers1cbc03f5refactor: hoist SYSTEM_CALLER read into ParallelExecutor::basic03c37d31refactor: hoist is_system_tx_gas_exempt helper to reth-chainspec3411118crefactor: use is_gravity_system_caller helper at sender comparison sites0d50df60feat: gas-exempt system transactions behind Alpha fork (L1+L2)7acf27effeat: zero SYSTEM_CALLER balance at Alpha activation (apply_state_change)ee261914feat(rpc): per-tx gas-exempt cfg for block-family trace endpoints274507c5feat(rpc): per-tx gas-exempt cfg for single-tx trace endpoints64c41f38refactor: drop fused_inspector machinery from GravityTracingCtxd1107e64perf(rpc): peek first prelude tx to seed replay EVM with matching cfgf8487835docs: clean up internal anchor references and stale doc paths31a9e5f4test: bundle equivalence + migration hook edge cases (load-bearing)86a068a9test(nice): fee-zero / balance-untouched + disable_balance_check defensive verify96eed10ftest(grep): long-term regression prevention checklist4d86493btest(e2e): Alpha-activation block + dual-backend system-tx gas-exempt parity7826cffatest(rpc): pre-Alpha block replay regression (no false exemption)8d73fcbftest(rpc): pure-simulation endpoints reject SYSTEM_CALLER spoofing348c5c05test(rpc): block-family + single-tx trace endpoints post-Alpha consistency339c0904test(rpc): tighten post-Alpha trace endpoints to byte-equal canonical (HP-1)f0afa12frefactor(ci): move grep invariants from Rust test to bash script + CI steped12dedbfix(ci): backtick SYSTEM_CALLER in is_system_tx_gas_exempt doc97728ad7fix(ci): drop tx.clone() in Trace::trace_block_with replay loopb8fe95a7test(rpc): pin system-txs-at-block-head invariant for trace replay loopsTest plan
WrapExecutor) and grevm (GrevmExecutor) ontransact_system_txn(crates/ethereum/evm/src/parallel_execute.rsU-6) — load-bearing serial==grevm invariant.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.disable_balance_checkzero collateral (U-8).gravity_system_tx_gas_exempt_test.rs).gravity_system_tx_pre_alpha_replay_test.rs).from=SYSTEM_CALLERfree-gas attempt (gravity_system_tx_simulation_anti_spoof_test.rs).gravity_system_tx_post_alpha_trace_test.rs).system_txs_form_head_prefix: 7 unit tests incrates/chainspec/src/gravity.rs#testscovering empty / user-only / system-only / normal / epoch-switch shapes + two violation shapes (system-after-user / user-first-then-system).scripts/check-gravity-invariants.sh, wired into theintegrationworkflow): system-tx callsites count, SYSTEM_CALLER address single-definition, RPC historical-state gating,execute_history_blockcaller audit, single-predicate referenced by all layers.Compatibility notes
falseforblock_ts < alphaTime. RPC replay regression test pins this.gas_used > 0butreceipt.gas_price = 0and no balance delta — explorers may want to label these distinctly.Not in this PR (deferred follow-ups)
parallel_apply_transitions_and_create_revertsaccounting fix (reverts_sizeis never updated, while revm serial updates bothstate_sizeandreverts_size). Discovered by U-6 bundle equivalence test; non-consensus (state root unaffected, onlyBundleState::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/main6ea2ba328c(PR #366, randomness-by-height precompile gas guard). Backup of pre-shrink tip atfeat/system-tx-gas-exempt-pre-shrinklocally (29 commits, included BLS+R9 trio + their CI hygiene fixes — now extracted into #370 / #371).