Skip to content

fix(rpc): mint precompile RPC registration gap + post-Alpha trace test design gap (same class as BLS §3.5.0) #372

Description

@nekomoto911

TL;DR

Two entangled bugs, both must be fixed before Alpha mainnet activation:

  • Track A — RPC mint precompile registration gap: register_custom_precompiles in crates/rpc/rpc/src/eth/helpers/call.rs:108-145 never registers the mint precompile (NATIVE_MINT_PRECOMPILE_ADDR = 0x...01625f5000), while the pipe layer registers it unconditionally for every block via transact_system_txn's system_precompiles vec (crates/pipe-exec-layer-ext-v2/execute/src/lib.rs:752-757). Any debug_trace* / trace_* replay whose execution reaches a CALL to that address will produce output that byte-diverges from canonical execution. Same class as the BLS RPC gap closed in commit 23a55587 / gravity-audit §3.5.0 / PR fix(rpc): register BLS pop-verify precompile unconditionally (split from #367) #370.

  • Track B — post-Alpha trace test design gap: the existing gravity_system_tx_post_alpha_trace_test.rs claims "byte-equal canonical for blocks containing system txs" but only exercises a degenerate fixture path that never reaches the mint precompile (or any other deeply-nested system contract path). The mint gap above sits beneath this test's coverage — the test "PASSes" without ever testing what it claims to test. This test design is the reason past review missed the BLS gap too, and will continue to miss future similar regressions until the fixture matrix is widened.

The two are entangled: fixing only Track A still leaves the trace test unable to catch future analogous gaps. Fixing only Track B exposes Track A as a hard failure. Both must land together.


Track A — RPC mint precompile registration gap

Reachability

The gap is fork-independent at the protocol level (pipe always registers; RPC never registers). What changes between forks is the surface area of on-chain consumers:

Fork Surface Trigger frequency
pre-Alpha Only direct user-EOA tx with to=0x...01625f5000 Near-zero in practice — no system contract bytecode references the address; AUTHORIZED_CALLER (the precompile's caller-allowlist target) is not deployed, so even a direct call would revert on the pipe side and merely return 0 on RPC. The divergence is real (gas_used + receipt status) but the realistic traffic count is likely zero.
post-Alpha Routine system-contract chain: metadata tx → BLOCK_ADDR.onBlockStart → GENESIS_ADDR → mint Every block that activates a non-trivial onBlockStart branch (failed proposer, epoch transition, reward distribution) hits this chain.

Bytecode evidence

Scanning crates/pipe-exec-layer-ext-v2/execute/gravity_hardfork.json (post-Alpha genesis) for the literal PUSH5 0x01625f5000 + GAS + CALL sequence (826401625f50005af1):

  • 0x00000000000000000000000000000001625f0001 (GENESIS_ADDR): pre-Alpha 4188 B → post-Alpha 6557 B (+2.4 KB, mint call added). 1 mint CALL site.
  • 0x595475934ed7d9faa7fca28341c2ce583904a44e (AUTHORIZED_CALLER, the precompile caller-allowlist target): pre-Alpha NOT_ALLOCATED → post-Alpha 1313 B (newly deployed). 1 mint CALL site.

Both mint CALL sites have byte-identical ~80-hex surrounding context — the same safeCall(mint, recipient, amount) Solidity helper inlined into both contracts.

GENESIS_ADDR is a hub — 7 system contracts reference it via PUSH5 0x01625f0001 (6401625f0001):

Caller PUSH5 refs
BLOCK_ADDR (0x...01625f2004) — metadata system tx target 2
VALIDATOR_MANAGER_ADDR (0x...01625f2001) 2
RECONFIGURATION_ADDR (0x...01625f2003) 2
PERFORMANCE_TRACKER_ADDR (0x...01625f2005) 2
NATIVE_ORACLE_ADDR (0x...01625f4000) 2
JWK_MANAGER_ADDR (0x...01625f4001) 2
STAKING_ADDR (0x...01625f2000) 1

Confirmed call chain: metadata tx (sender=SYSTEM_CALLER, to=BLOCK_ADDR) → BLOCK_ADDR.onBlockStart → CALL GENESIS_ADDR.<selector> → CALL 0x...01625f5000 (mint).

AUTHORIZED_CALLER exposes 3 selectors (2e996a8a, 9b8bd103, fab32b26) which are a strict subset of GENESIS_ADDR's 4 (2e996a8a, 6032f7e8, 9b8bd103, fab32b26) — strongly suggesting GENESIS_ADDR is a dispatcher that forwards those 3 to AUTHORIZED_CALLER, which is the actual contract on the precompile's caller-allowlist.

Fix

In crates/rpc/rpc/src/eth/helpers/call.rs's register_custom_precompiles, add an unconditional mint registration alongside the existing unconditional BLS registration (lines 122-123 today after PR #370 lands):

// Mint precompile is registered unconditionally to mirror the pipe layer's
// `system_precompiles` registration in `transact_system_txn` (post-Alpha
// `metadata tx → BLOCK_ADDR.onBlockStart → GENESIS_ADDR → mint` is routine;
// pre-Alpha narrow user-EOA-direct-call surface still benefits — same logic
// as the BLS unconditional registration).
let mint_precompile = gravity_precompiles::mint_token::create_mint_token_precompile();
evm.precompiles_mut().apply_precompile(
    &gravity_precompiles::mint_token::NATIVE_MINT_PRECOMPILE_ADDR,
    move |_| Some(mint_precompile),
);

Requires moving mint_precompile.rs from crates/pipe-exec-layer-ext-v2/execute/src/ to crates/gravity-precompiles/src/mint_token.rs, and the NATIVE_MINT_PRECOMPILE_ADDR const from crates/pipe-exec-layer-ext-v2/execute/src/onchain_config/mod.rs:94 to live alongside (or be re-exported from) the new module. Same hygiene refactor pattern as commit fe73b70c for BLS.

Pipe-layer dep on the mint module stays prod (it's the pipe's system_precompiles source); RPC takes a prod dep on gravity-precompiles (already a dep — randomness_by_height lives there).

Long-term guard

Add a grep invariant to scripts/check-gravity-invariants.sh: assert that crates/rpc/rpc/src/eth/helpers/call.rs contains a registration call for each address that appears in the pipe's system_precompiles vec. Today: BLS + mint. Future: any new system precompile gets caught at CI time, not in production.


Track B — post-Alpha trace test design gap

What the test claims

gravity_system_tx_post_alpha_trace_test.rs::test_rpc_post_alpha_trace_consistency_* is described as "block-family + single-tx trace endpoints byte-equal canonical execution for blocks containing system txs". The PR body marks it ✅ and reviewers (justifiably) read that as "trace fidelity for post-Alpha system-tx blocks is pinned".

What the test actually does

empty_ordered_block fixture:

  • transactions: vec![]
  • senders: vec![]
  • extra_data: vec![]
  • failed_proposer_indices: vec![]
  • proposer_index: Some(0)

Pipe layer injects exactly 1 metadata system tx whose execution is BLOCK_ADDR.onBlockStart(proposerIdx=0, failedProposers=[], ts). With no failed proposers and no extra event hooks, onBlockStart short-circuits without ever calling GENESIS_ADDR.<selector>. Thus mint is never reached. Thus the mint-not-registered gap doesn't surface as a test failure.

The test verifies "byte-equal canonical for the degenerate path where the pipe's metadata tx returns without any deeply-nested side effect". It does not verify "byte-equal canonical for blocks the way they show up in production".

Why this matters beyond mint

Every dimension of "what production blocks actually exercise" that the fixture doesn't span is a class of gap the test cannot catch:

Dimension Variants needed Code path covered
failed_proposer_indices vec![] / vec![non_empty] onBlockStart reward / slashing branches → GENESIS_ADDR → mint
epoch transition regular block / epoch-end block / new-epoch-start block RECONFIGURATION_ADDR + DKG state advance
extra_data validator txns vec![] / [DKG] / [JWK] / [DKG, JWK] DKG_ADDR / JWK_MANAGER_ADDR / AUTHORIZED_CALLER chains
proposer_index Some(N) / None (NIL block) block proposer accounting path

Today's fixture covers exactly 1 cell ([], regular, [], Some(0)) out of the matrix.

Proper test design

run_post_alpha_trace_consistency should accept a fixture descriptor and assert byte-equal canonical for each combination. Minimum viable extension to catch the mint gap: add a single fixture with failed_proposer_indices: vec![non_empty_idx] — this triggers BLOCK_ADDR.onBlockStart's failed-proposer branch, which routes through GENESIS_ADDR, which calls mint.

The harder dimensions (DKG / JWK extra_data) require constructing realistic protocol event payloads with valid signatures. The current 21 test files across pipe-exec-layer-ext-v2/execute/tests/ all use extra_data: vec![]; no helper exists for constructing DKG/JWK events. Two paths:

  • (a) Heavyweight, faithful: build mock DKGStartEvent encoder + dummy signature path + JWK upsert payload builder. Reusable test infra for any future epoch / validator test.
  • (b) Lightweight, faster: drop test scope one layer down — mock the post-execution block directly (synthesize a block.body.transactions vec with metadata + synthetic validator txs whose senders are forced to SYSTEM_CALLER), feed into RPC trace path. Loses end-to-end fidelity (pipe's tx construction is bypassed) but gains coverage of the RPC replay path's behavior on the production-shape block.

(a) is the right long-term shape. (b) is acceptable as an incremental step.

Why this is a design bug, not just missing coverage

If it were just "we forgot to add fixtures", the test infra would already support DKG/JWK and we'd just add cells. But the test infra cannot construct those fixtures, and the assert_*_byte_equal_canonical runner takes no parameters that would let us vary the dimension. The runner is hard-coded for the degenerate path. The test was effectively designed in a way that makes broadening impossible without harness work — and that's what reviewers should be reading the absence of as a flag, but they don't because the PR body markets it as "post-Alpha trace consistency PASS".


Recommended packaging

Upgrade PR #370 (currently 3 commits: BLS RPC register + hygiene move + replay test) into a "BLS + mint dual RPC precompile registration fix" PR. Add:

  1. Move mint_precompile.rscrates/gravity-precompiles/src/mint_token.rs + addr const re-home (mirrors BLS commit fe73b70c).
  2. Unconditional mint registration in crates/rpc/rpc/src/eth/helpers/call.rs::register_custom_precompiles (mirrors BLS commit 23a55587).
  3. Add the failed_proposer_indices: vec![non_empty] fixture cell to gravity_system_tx_post_alpha_trace_test.rs and assert byte-equal canonical — exercises GENESIS_ADDR → mint end-to-end (mirrors the BLS replay byte-equal-canonical test added in commit 2f19734c).
  4. Add the grep invariant in scripts/check-gravity-invariants.sh asserting call.rs registers each system_precompiles address.

The broader test harness work (DKG/JWK fixture infrastructure for full matrix coverage) is deferred to a follow-up PR — it's heavyweight enough to warrant its own scope, and the minimum-viable mint-catching fixture above is what unblocks Alpha mainnet readiness.

Compatibility

  • Read-only side only — no consensus, state, or storage impact
  • No RPC API surface change (registration is transparent to callers)
  • No data migration

Priority

P1 — not a P0 blocker for the Alpha hardfork ship itself (this is RPC-only, doesn't affect consensus or write path), but must merge before Alpha mainnet activation:

  • post-Alpha real blocks routinely exercise the GENESIS_ADDR → mint chain (every block with failed_proposer / epoch transition / reward distribution)
  • archive nodes replaying any such block emit trace bytes that disagree with canonical
  • external indexers / explorers / dApp tooling consuming debug_trace* / trace_* data diverge from on-chain truth

Out of scope for this issue


Track C — CI allowlist gap (root cause of Track B's "test PASSes" claim)

Added after empirical discovery during PR #377 (which fixes an independent
debug_traceTransaction cfg bug shipped by #367 and wires the deferred tests
to CI).

The gap

.github/workflows/integration.yml's gravity-pipe-test job uses an explicit --test <name> allowlist because the crate contains integration binaries with dev-dep gaps in this workspace (e.g. gravity_hardfork_test.rs depends on reth-ethereum-forks which isn't in dev-dependencies). Before #377, that allowlist was:

--test gravity_pipe_test
--test gravity_eip2935_test
--test gravity_eip7702_test

Every RPC replay integration test file added by #367 and #370gravity_system_tx_gas_exempt_test, gravity_system_tx_pre_alpha_replay_test, gravity_system_tx_simulation_anti_spoof_test, gravity_system_tx_post_alpha_trace_test, gravity_bls_precompile_test — was never added to that list. They compiled locally in dev but CI silently skipped them (both compile and run). The - [x] checkboxes for those tests in #367 / #370 PR bodies were author self-attestation, not CI-gated.

This is the root cause of Track B being a "test PASS without ever running": the test file existed, its assertions were correct in principle, but the runner never touched it. Track B's "degenerate fixture" critique is downstream — even if the fixture had hit the mint chain, CI wouldn't have caught it either.

Independent discovery

Reproduced empirically while iterating on #377: running cargo nextest run -p reth-pipe-exec-layer-ext-v2 --test gravity_system_tx_post_alpha_trace_test --retries 0 locally on upstream/main (with a clean datadir) reveals two distinct failure modes, both of which CI would have caught if the test had been wired:

  1. GasPriceLessThanBasefee on debug_traceTransaction for the metadata system tx — an independent cfg gap in crates/rpc/rpc/src/debug.rs::debug_trace_transaction where the target tx's evm_env is never toggled with disable_base_fee = true. Symmetric fix already exists at crates/rpc/rpc-eth-api/src/helpers/trace.rs:253-266 for the parity-namespace path; debug-namespace was skipped. Fixed by PR fix(rpc): debug_traceTransaction target-tx gas-exempt gap + wire deferred #367 tests #377.
  2. ~9.4k gas divergence in trace_block(1)[0] on the Alpha activation block's metadata tx (RPC 292093 vs canonical 282665) — precisely the mint-precompile-not-registered symptom described in Track A, plus a possible RPC-side gap for the apply_state_change Alpha migration hook (canonical zeroes SYSTEM_CALLER.balance at block 1 via the pipe-layer hook; RPC replay may not run the same hook, leaving state inconsistent for subsequent execution).

Fix (partial, in PR #377)

PR #377 does two things related to Track C:

  1. Wires the three tests that currently pass (gravity_system_tx_gas_exempt_test, gravity_system_tx_pre_alpha_replay_test, gravity_system_tx_simulation_anti_spoof_test) to the workflow's --test allowlist.
  2. Adds invariant 9 to scripts/check-gravity-invariants.sh: every gravity_system_tx_*_test.rs / gravity_bls_*_test.rs file must be EITHER wired to CI OR listed in KNOWN_UNWIRED_TESTS with a documented reason. Also flags double-claims and stale skip-list entries. Any future test file forces the author to make an explicit CI-wiring decision — no more silent skips.

gravity_system_tx_post_alpha_trace_test remains deferred in KNOWN_UNWIRED_TESTS with reason "blocked on #372 mint precompile RPC registration + RPC-side Alpha migration hook". Wiring it is the acceptance criterion for closing this issue.

Recommended addition to #372's scope

The Track A fix now has a concrete regression test to wire on landing: remove gravity_system_tx_post_alpha_trace_test from KNOWN_UNWIRED_TESTS in check-gravity-invariants.sh and add --test gravity_system_tx_post_alpha_trace_test to integration.yml. Invariant 9 will fail CI if this is forgotten.


关联的 failing test cases

These test cases are currently blocked on the fixes in this issue. When Track A (mint precompile RPC registration) + the RPC-side Alpha migration hook land, the CI wiring can be updated to include them by removing them from KNOWN_UNWIRED_TESTS in scripts/check-gravity-invariants.sh and adding --test gravity_system_tx_post_alpha_trace_test to .github/workflows/integration.yml (invariant 9 introduced in PR #377 gates this — see Track C).

  • crates/pipe-exec-layer-ext-v2/execute/tests/gravity_system_tx_post_alpha_trace_test.rs
    • test_rpc_post_alpha_trace_consistency_grevm (line 867) — fails on ~9.4k gas divergence at block N metadata tx (block-family trace, RPC 292093 vs canonical 282665) due to mint precompile not registered on RPC side (Track A) and possibly the RPC-side Alpha migration hook gap (see Track C §"Independent discovery" point 2).
    • test_rpc_post_alpha_trace_consistency_disable_grevm (line 877) — same failure, grevm-disabled execution path.

Any additional test cases surfaced by the fixture-fix work (tracking #367 follow-up) will be appended here when confirmed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmainnet

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions