Skip to content

test(rpc): equivalence of execution vs header randomness providers (split from #367)#371

Open
nekomoto911 wants to merge 1 commit into
Galxe:mainfrom
nekomoto911:test/r9-randomness-provider-equivalence
Open

test(rpc): equivalence of execution vs header randomness providers (split from #367)#371
nekomoto911 wants to merge 1 commit into
Galxe:mainfrom
nekomoto911:test/r9-randomness-provider-equivalence

Conversation

@nekomoto911

Copy link
Copy Markdown
Collaborator

Summary

Pin the byte-equality of the two RandomnessByHeightProvider implementations with a unit-level equivalence test. Single commit factored out of #367 — not dependent on Gravity Alpha.

Why split out of #367

The randomness_by_height precompile is registered in two places with intentionally distinct data sources:

  • pipe ExecutionRandomnessProvider (crates/pipe-exec-layer-ext-v2/execute/src/randomness_precompile.rs) reads the in-flight OrderedBlock.prev_randao for the current block, parent_header.mix_hash() for the parent, and a GravityStorage-backed fallback for older heights
  • RPC HeaderRandomnessProvider (crates/rpc/rpc/src/eth/helpers/call.rs) reads HeaderProvider::header_by_number(h).mix_hash for past heights and uses the EVM env's prev_randao (passed as current_randomness) for the simulated block

The two diverge on data source by necessity (the in-flight block is not persisted while it is executing) but the shared trait RandomnessByHeightProvider requires they produce byte-identical RandomnessByHeightLookups on the same (block_number, query_height) input for RPC replay (debug_trace* / trace_*) to stay byte-equal canonical on any historical block calling 0x...625f5002.

Same class of pipe/RPC gap as the BLS RPC-registration bug — pinning it with a unit test is independent of Alpha.

What

Adds execution_provider_and_header_provider_agree_byte_for_byte in crates/rpc/rpc/src/eth/helpers/call.rs#tests. Feeds both providers the same in-memory dataset and asserts RandomnessByHeightLookup byte-equality across 11 query_height boundaries: 0, 1, current / 2 (a height with no header), current - window - 1, current - window, current - window + 1, current - 2, current - 1 (parent), current, current + 1, u64::MAX. The fallback (pipe GravityStorage) vs header_by_number(h).mix_hash (RPC) data-source agreement is a separate engine-API ingestion invariant, not under test here.

Adds reth-pipe-exec-layer-ext-v2.workspace = true to crates/rpc/rpc/Cargo.toml's [dev-dependencies] so both provider types are in scope from the #[cfg(test)] module (HeaderRandomnessProvider is local to reth-rpc; ExecutionRandomnessProvider lives in pipe-exec-layer).

Test plan

  • Local cargo check -p reth-rpc --tests
  • CI: cargo nextest run -p reth-rpc execution_provider_and_header_provider_agree_byte_for_byte

Compatibility

Test-only + dev-dep. No prod code changes.

Coordination

R9 from `_local/drafts/system-tx-gas-exempt/system-tx-gas-exempt-design-2026-06-25.md`
§3.5.0 / §6.2 / §7 decision 6: the `randomness_by_height` precompile is
registered in two places with intentionally different data sources:

- pipe `ExecutionRandomnessProvider` (`crates/pipe-exec-layer-ext-v2/execute/src/randomness_precompile.rs`)
  reads the in-flight `OrderedBlock.prev_randao` for the current block,
  `parent_header.mix_hash()` for the parent, and a `GravityStorage`-backed
  fallback for older heights.
- RPC `HeaderRandomnessProvider` (`crates/rpc/rpc/src/eth/helpers/call.rs`)
  reads `HeaderProvider::header_by_number(h).mix_hash` for past heights and
  uses the EVM env's `prev_randao` (passed as `current_randomness`) for the
  simulated block.

The two implementations diverge on data source by necessity (the in-flight
block is not persisted while it is executing) but must produce byte-identical
`RandomnessByHeightLookup`s on the same `(block_number, query_height)` input
for RPC replay (`debug_trace*` / `trace_*`) to stay byte-equal with canonical
on any historical block containing a `0x...625f5002` call. Any divergence is
the same class of pipe/RPC gap as the BLS RPC-registration bug closed in
23a5558 — a silent state-root / receipts fork.

Verify trail:

- Both providers already implement the shared `RandomnessByHeightProvider`
  trait from `gravity-precompiles` (return type
  `Result<RandomnessByHeightLookup { value: Option<B256>, gas_used: u64 }, _>`),
  so route B applies: equivalence test only, no refactor.
- Logic walk-through confirmed equivalence for the block-replay case
  (`current_randomness = Some(_)`) across every branch in both impls when
  the data sources are consistent:
  - `current_randomness`: pipe sets from `ordered_block.prev_randao`; RPC
    sets from EVM env. For replay these must agree by construction.
  - `parent_randomness` (pipe) vs `header_by_number(parent).mix_hash` (RPC):
    pipe wires `parent_header.mix_hash()` in
    `crates/pipe-exec-layer-ext-v2/execute/src/lib.rs:390`, so both read the
    same value at the parent.
  - Fallback (pipe `GravityStorage`) vs `header_by_number(h).mix_hash` (RPC):
    both source from the persisted header chain; data-source agreement is a
    separate engine-API ingestion invariant, not under test here.
- One pathological edge identified: with `recent_window = 0`, pipe always
  treats parent as recent (special-cased) while RPC treats parent as storage
  tier (`reference - parent = 1 > 0`). No real policy uses `recent_window = 0`
  (Alpha default is 86_400). Not tested; documented in this commit body.

Test:

`execution_provider_and_header_provider_agree_byte_for_byte` feeds both
providers the same in-memory data set and asserts `RandomnessByHeightLookup`
byte-equality across 11 `query_height` points covering: 0, 1,
`current / 2` (a height with no header), `current - window - 1`,
`current - window`, `current - window + 1`, `current - 2`, `current - 1`
(parent), `current`, `current + 1`, and `u64::MAX`. PASS.

Placement: ideally the test lives in
`crates/pipe-exec-layer-ext-v2/execute/src/randomness_precompile.rs#tests`
next to `ExecutionRandomnessProvider`, but `HeaderRandomnessProvider` is a
private struct local to `crates/rpc/rpc/src/eth/helpers/call.rs`. Moving it
to a shared crate would be a refactor that R9 did not require (the shared
trait already exists), so the test lives in the `reth-rpc` crate where both
types are in scope (`reth-pipe-exec-layer-ext-v2` is already a prod dep of
`reth-rpc`).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant