Skip to content

feat(rex7): checkpoint compute gas settlement with gas clamp enforcement - #367

Open
RealiCZ wants to merge 211 commits into
cz/chore/upgrade-revm-40from
cz/feat/rex7-checkpoint-gas
Open

feat(rex7): checkpoint compute gas settlement with gas clamp enforcement#367
RealiCZ wants to merge 211 commits into
cz/chore/upgrade-revm-40from
cz/feat/rex7-checkpoint-gas

Conversation

@RealiCZ

@RealiCZ RealiCZ commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Rex7 replaces per-opcode compute-gas recording with checkpoint settlement, and replaces post-opcode limit checking inside plain segments with gas-clamp enforcement. Roughly 140 plain opcodes now dispatch to revm's own instructions with no wrapper at all: the interpreter's gas counter is the accounting source, and compute gas settles as a segment delta at each checkpoint. Enforcement inside a segment is delegated to revm's own per-opcode gas check by hiding the gas above the remaining compute headroom, so a limit-crossing opcode is stopped before it executes rather than being caught after it has already run.

The checkpoints are exactly the positions that had to stay wrapped anyway — the storage-gas opcodes, the CALL / CREATE family, the volatile / detention opcodes, GAS, and frame entry / resume / exit — so the change removes metering cost without adding any new one. The interpreter_hotloop benchmark drops from 1.81 ms to 0.96 ms (−47%), which is the vanilla-revm floor for that workload.

For a transaction that stays inside every resource limit and in which no frame ends in an exceptional halt, Rex7 is bit-identical to Rex6: same gas, same receipt, same state, same GAS readings, same recorded compute total. Segment sums telescope to the per-opcode sums exactly.

Rex7 is the unstable spec and is not scheduled on any network.

What changed

Checkpoint settlement. The Rex7 instruction table starts from revm's own table and overrides only the checkpoint entries. Each checkpoint opens with checkpoint_prologue! — settle the open segment as baseline − remaining, hand the clamp-hidden gas back so the body runs on the true counter, re-open the window — and closes with checkpoint_epilogue!, which re-applies the clamp against the freshly settled usage. Storage-gas charges are excluded from the open segment as they are taken, so the exclusion survives a body that aborts before its own measurement window closes. A checkpoint body that halts leaves its own already-taken charges — a value-transfer fee, argument-range memory expansion — inside the open segment, and the frame-exit settlement records them as executed work; the epilogue only re-clamps a frame that keeps executing.

Gas clamp. At each checkpoint exit, frame entry, and frame resume, interpreter-visible gas is clamped to min(frame remaining compute budget, tx-level remaining under the effective limit). The constraint that bound the clamp is captured at the moment it is applied, so a clamp-induced out-of-gas is classified against what was actually in force: frame-local budget becomes a frame revert with MegaLimitExceeded carrying the frame's own budget, transaction-level becomes an OutOfGas halt with gas rescue, and a detained limit becomes VolatileDataAccessOutOfGas with the same rescue. The clamp is unobservable to a transaction that never exceeds a limit: GAS, call-gas forwarding, and storage-gas charges all see the restored counter.

Exceptional-halt carve-out. A frame that ends in an exceptional halt returns none of its budget, so that budget has to be settled as compute gas — but not as one number. The executed part (the open segment, less any storage gas a checkpoint body charged before aborting) records through the ordinary enforcing path, because a parent frame keeps executing after absorbing a failed child and leaving that work out of enforcement would let the following code spend the same headroom twice. The destroyed part (whatever the frame still held when its result became final) is reported and accumulated but never compared against any limit, at transaction level or block level — enforcing it would turn an ordinary EVM halt into a resource-limit failure with the gas rescued, changing a receipt this carve-out requires to keep identical.

The split is taken from the frame's final result, after the create-return processing that can still turn a successful constructor into a code-deposit out-of-gas, an EIP-3541 reject, or a runtime code-size reject.

A frame refused at initialization is part of the same split: a refusal that swallows the forwarded budget (an address collision) books that budget as destroyed at the refusal site, while refund-class refusals (depth, balance, nonce) book nothing — their gas returns to the caller's envelope.

Conservation-law reporting. The reported compute_gas_destroyed is not the sum of the sites that destroyed it: it is derived once per transaction at settlement as destroyed = tx_gas_spent + minted_call_stipend − non_compute_gas − enforced_compute, where minted_call_stipend counts the 2,300 revm mints into a value-transferring CALL/CALLCODE child budget (per mint, including invocations turned away at frame entry — the mint flows back into the envelope with the refund). Any path that burns an envelope — known or future — is captured by the law without needing a recording call. The per-site bookings remain as the enforcement split and as a debug_assert cross-check that holds the derivation and the sites to each other; the law was validated over every transaction the test corpus executes (zero deviations) plus the mainnet replay fixtures under Rex7, and a negative derivation saturates to zero in release while asserting in debug. Enforcement never reads the derived value: transaction limits run on the per-opcode lane and block admission on the new compute_gas_enforced.

Failed-deposit envelope settlement. An OP deposit is not allowed to fail: op-revm rewrites any failed deposit — a validation reject or an execution halt — into a FailedDeposit receipt that reports the whole gas limit, after every Mega settlement has already run. That rewrite is now a settlement boundary of its own: the difference between the rewritten envelope and what the lanes already hold is booked as destroyed and the derivation is re-settled, so the reported total covers the receipt while enforcement stays untouched — a rejected deposit must not consume block compute capacity for work it never performed. A debug-only terminal reconciliation at outcome construction asserts that the lanes account for every receipt's envelope on every Rex7 transaction, so the next post-settlement envelope rewrite — wherever it comes from — trips on its first transaction instead of shipping silently.

Precompile accounting keys on identity. The KZG fixed-fee accounting arm keys on the dispatched precompile's PrecompileId, not just its address (Rex7 only; frozen specs keep the address-only match). A dynamic override registered at the KZG address therefore falls through to the generic halt arm instead of being priced as wired KZG work.

Code-deposit compute gas is weighed before it is recorded. Rex7 settles a CREATE's canonical code-deposit compute charge after the frame's other dimensions have settled and before revm commits the CREATE checkpoint, and records it only for a deposit that actually happens: a charge that would exceed the frame's compute budget rewrites the result to the same MegaLimitExceeded revert the late absorb arm produces — with the journal now rolling back consistently — and a transaction-level exceed keeps the existing halt-with-rescue path (a simultaneous exceed of both classifies frame-local, matching Rex5/Rex6). Rex4–6 keep their historical recording point and behavior, including Rex6's unconditional record. The charge is read off the configuration's active gas schedule, the same source return_create debits.

The gas schedule is owned by the spec. CfgEnv::gas_params is not an embedder surface on MegaETH: every construction and adoption path, plus a per-transaction check that also covers live modify_cfg mutation, rejects a schedule that deviates from the spec-defined table with a loud panic naming the first differing entry — and rejects a CfgEnv::spec that disagrees with the context's own spec, which would otherwise run one transaction under two specs at once. MegaETH accounting sites may therefore read revm's schedule constants, which the pin proves equal to the active table.

Spec migration rebuilds the limit tracker. AdditionalLimit latches spec-derived flags at construction, and MegaContext::with_cfg used to keep every latch from construction time when the incoming cfg migrated the spec. It now rebuilds the tracker from the new spec (keeping the configured runtime limits) so the latched state cannot diverge from the context's spec, pinned by migration regression tests in both directions and both builder orders. Checkpoint gating itself stays a runtime spec check inside the shared handlers, matching the upstream revm idiom; the frame-dense bench_subcall microbenchmarks for the frozen specs pay a small instruction-count overhead for those checks, which is acknowledged — realistic-shape benchmarks are unaffected.

Public API changes (mega-reth integration surface)

  • MegaTransactionOutcome gains compute_gas_destroyed: u64 (reported statistic, derived from the conservation law) and compute_gas_enforced: u64 (the number the transaction's own enforcement ran on).
  • BlockLimiter splits compute gas into two counters: block_compute_gas_used (full reported total, semantics unchanged) and the new block_compute_gas_enforced (the counter block admission compares).
  • BlockLimiter::post_execution_update_raw takes compute_gas_enforced as a new parameter (8 → 9 arguments); block admission accumulates it directly rather than reconstructing it by subtraction.
  • New sandbox::SandboxUsage { usage: LimitUsage, burned_compute_gas: u64 }; SandboxOutcome::Completed.limit_usage changes type accordingly.
  • MegaBlockLimitExceededError::ComputeGasLimit.block_used now reports the enforced reading — the counter that was actually compared.
  • with_cfg / with_cfg_unpinned / new_with_context and the per-transaction entry now panic on a CfgEnv whose gas_params deviate from the spec-defined schedule, or whose spec disagrees with the context's spec. The previously carried ability to install a custom gas schedule through CfgEnv::gas_params is withdrawn — a schedule change is a spec change.

A consumer that accumulates compute usage into any further limit must use compute_gas_enforced; compute_gas_used is the reported statistic and carries destroyed remainders.

Deliberate deviations from Rex6

Each of these is documented in docs/spec/upgrades/rex7.md:

  1. Exceptional halts report more compute gas. A transaction that halts exceptionally, or that contains an inner frame which does, may report a strictly higher compute total than under Rex6. EVM gas, the receipt, and the outer transaction's success or failure are unchanged.
  2. One shape enforces more strictly. An ordinary out-of-gas taken with no clamp in force arrives at frame exit with the interpreter's counter already zeroed by revm, so the whole segment measures as executed and is enforced in full. The split cannot be recovered there, and the fail-closed reading is the one that does not let destroyed budget escape accounting.
  3. Top-frame tie-break. When the frame's remaining compute budget equals the transaction-level remaining budget, the clamp binds to the transaction-level constraint, so the exceed halts with gas rescue. Rex6 classifies the same equality as frame-local, which the top-level frame absorbs into a revert.
  4. Double-exceed preference. When the crossing opcode would exhaust both the true remaining EVM gas and the compute headroom, the halt is attributed to the compute limit, which is the classification that preserves the sender's refund.
  5. actual may exceed limit. The actual on a compute-gas halt reason is the transaction's full reported total, which carries destroyed remainders that were never enforced.
  6. Failed deposits account for the rewritten envelope. A failed deposit's receipt reports the whole gas limit; Rex7 books the gap between that envelope and the recorded lanes as destroyed, so compute_gas_used covers the receipt. Rex6 has no destroyed lane and keeps its frozen accounting. Enforcement and the receipt itself are unchanged on both.
  7. Code-deposit compute gas is conditional. Rex7 records a CREATE's 200-per-byte code-deposit compute charge only when the deposit actually happens; Rex6 records it unconditionally, including for a frame its own charge pushed over the limit, so Rex7 reports and enforces less on that path. Receipts and EVM gas are unchanged.

Testing

New suites under crates/mega-evm/tests/rex7/ (169 tests): checkpoint settlement, gas-clamp enforcement, the executed/destroyed burn split, exceptional halts, clamp classification, gas-leakage paths under an active clamp, latch surfacing, interceptor and precompile resume settlement, Rex6/Rex7 parity across transaction shapes, the double-exceed corner, a parity case for every checkpoint opcode, conservation-law term combinations (multiple mints, mint plus destroyed remainder, negative sandbox residue), the precompile / KeylessDeploy / pre-execution synthetic-halt splits, the failed-deposit receipt rewrite, and dyn-precompile halt accounting under the identity key. Every transaction the Rex7 suite executes is additionally reconciled lane-for-lane against its receipt envelope in the shared test harness. The conditional code-deposit charge has its own four-row suite (create_code_deposit_charge), and the schedule/spec pins carry paired-mutation and per-construction-path rejection tests. Frame-init refusals have their own class matrix (frame_init_reject_burn), including the deposit-rewrite and precompile double-count exclusions. Halting call bodies carry a differential suite (call_body_halt_charges) pinning that their taken charges settle as executed on both debug and release profiles. Block-level lane separation is covered by tests/block_executor/compute_gas_lanes.rs.

cargo test -p mega-evm is green across all 14 test binaries. Spec-migration parity with direct construction (with_cfg in both directions and both builder orders) is pinned by regression tests in crates/mega-evm/src/evm/context.rs.

Verification tooling (in this PR)

The branch carries the harness that produced its own strongest evidence, so a reviewer can re-run it rather than trust it.

Differential gate. state-test --bench-spec Rex7 --diff-spec Rex6 executes every fixture under both specs and classifies each transaction vector: identical → PASS; different with execution-provenance evidence of a Mega mechanism (an inspector-observed halted frame, a typed Mega halt, a tracker counter — revert payloads are recorded but never license anything, and the pair is locked to Rex7/Rex6 because the precision invariant authorizes no other) → EXPLAINED; different without such evidence → UNEXPLAINED, hard red.

Result over the EEST corpus (v5.4.0 fixtures_stable, 44,023 transaction vectors): PASS 19,611 / EXPLAINED 17,363 / UNEXPLAINED 0 / PANIC 0, with every explained difference confined to compute_gas_used — no consensus-surface divergence anywhere in the corpus. A debug sweep of the same corpus also runs the conservation and terminal-reconciliation asserts on every vector; it found (and this branch fixed) the failed-deposit, create-collision, and halting-call-body accounting gaps before any of them could ship.

Nightly. tools/eest-sweep/run.sh pins the corpus by release and sha256, verifies the unpacked tree against a per-file manifest, and eest-nightly.yml gates on PANIC = 0 and UNEXPLAINED = 0 (scheduled workflows fire once this lands on the default branch). A 10-case cache-integrity suite runs per PR.

Known-minor residuals, disclosed rather than churned: the corpus manifest authenticates against accidental damage, not an attacker with cache write access; two declared guards (irregular-entry scan, expect-exception counting) lack dedicated regression tests; interrupted runs can leave a lock that costs a later run its 15-minute wait; one summary line says "unit(s)" where it counts vectors; one doc line overstates cross-mode tally equality for corpora with multi-vector units (EEST v5.4.0 has none).

Notes for reviewers

This branch is stacked on #365 (revm 40.0.3 upgrade) and targets cz/chore/upgrade-revm-40, so the diff here excludes the revm upgrade itself.

Marked WIP: the semantics above are settled and implemented, but Rex7 is unfrozen and one integration question is still open — whether SandboxUsage's shape is the one mega-reth wants to consume. The OutOfGas-vs-MemoryOOG convergence question this note used to carry is settled: the unclamped side is deviation 2 above, and the clamp-induced sliver asymmetry (the sub-opcode visible remainder is burned on the OutOfGas path but restored on the MemoryOOG path) is acknowledged rather than converged — the sliver's size is unrecoverable once revm's cold path has zeroed the counter, and converging the other way would burn a refundable remainder — with reopen conditions recorded.

Measured-Inspector: rewriting inspectors are now fully supported (REX7)

Every inspector handed to MegaEvm is wrapped in a measurement shim. The EVM does not execute inside an inspector callback, so everything that changes across a callback boundary is the inspector's doing by construction; the shim snapshots the interpreter's working set (every constant-time reading), the pending action, the frame inputs and the outcomes, and books the differences on an InspectorLedger that travels out on MegaTransactionOutcome::inspector_ledger. Injected or removed gas never enters enforcement (the checkpoint baseline shifts by the same amount and the clamp is re-derived), so an inspector cannot buy a transaction headroom on any dimension. The conservation law gains the ledger's term and stays a debug assertion everywhere.

Frame settlement is now a single point: classify_frame_action / commit_frame_journal split revm's return_create, the frame loops park the REX7 journal decision on deferred_journal and carry it out after the last callback that can rewrite a result, so a frame's state always follows the result its caller is handed (this also closed a pre-existing frame-local exceed accounting defect). The canonical block path refuses any transaction whose ledger is non-zero (InspectorAdjustedAccounting, release-enforced, fails the block not the process). Two shapes are refused outright because upstream decides them before any callback can run: a failed creation rewritten into a success, and a classification rewrite of a result produced directly by frame init (empty-code value calls, precompiles, KeylessDeploy). TrustedObserver lets a type declared read-only in source skip the measurement (debug builds still measure and assert an empty ledger); production tracers pay revm-native cost, unknown inspectors pay full measurement.

Verification: per-shape cheat matrix machine-checked against the Inspector trait surface; state-test --mode chaos runs a seeded rewriting inspector over the full EEST corpus (44,023 vectors × 2 seeds, four failure counters at zero); the standard Rex7↔Rex6 differential stays field-for-field identical to its frozen baseline; two closed tables pin every gas-carrying field of revm's Gas/Interpreter/outcomes and every InstructionResult variant's destroyed disposition so an upstream bump that adds either fails to compile or fails a test. Frozen specs are bit-identical (EEST + Rex6 fill parity). Inspected-path benchmark rows (inspect_noop, inspect_tracer, and their _trusted variants) are added to transact; the declared-observer rows sit at revm-native cost, the measured rows carry the shim's per-callback snapshot.

RealiCZ added 30 commits August 11, 2026 14:54
Plain opcodes in the REX7 instruction table run revm's raw instructions with
no per-opcode recording; compute gas settles as an interpreter-gas delta at
each checkpoint (storage-gas opcodes, CALL/CREATE family, volatile opcodes,
frame entry/resume/exit). Per-transaction totals are unchanged; a limit
exceed now surfaces at the next checkpoint. Specs <= REX6 are untouched.
Covers plain segments, SSTORE/LOG, SLOAD, the CALL family (success, revert,
nested), CREATE/CREATE2, SELFDESTRUCT, volatile detention below the cap and
the GAS reading, each with minimum and scaled SALT buckets. Also pins the two
places the models differ: checkpoint-coarsened halts and out-of-gas frames.
At every checkpoint and frame entry/resume the interpreter's visible gas is
clamped to the compute headroom -- the tighter of the frame-local budget and the
TX-level detained limit -- and the hidden remainder is recorded together with the
constraint that bound it. revm's own per-opcode gas check then stops a crossing
opcode at the clamp boundary before it executes, so a plain-opcode segment is
bounded with no per-opcode accounting at all.

Checkpoint handlers gain a prologue (settle the open segment, restore the clamp
so CALL forwarding, GAS and storage charges observe the true counter) and an
epilogue (re-clamp against the possibly detained headroom). GAS joins the
checkpoint set so the clamp stays unobservable. The frame's final result restores
the hidden gas and reclassifies a clamp-induced out-of-gas as the compute exceed
it stands for: frame-local binding reverts to the parent, TX-level binding halts
with the gas rescued, and detention keeps its VolatileDataAccessOutOfGas
attribution.

Transactions that stay inside every limit remain bit-identical to per-opcode
accounting; a crossing now halts one opcode earlier, with that opcode's cost
excluded from the recorded usage. Specs <= REX6 are unchanged.
Pins that the clamp is unobservable through GAS, that a crossing opcode is
stopped before it executes with its cost excluded from usage, that a detention
cap is enforced inside a checkpoint-free loop, and that a clamp-induced
out-of-gas is reclassified by whichever constraint bound the clamp (frame-local
revert, TX-level halt with rescue, volatile-detention attribution) including the
double-exceed corner where the compute classification wins.

The checkpoint-settlement suite's enforcement case is updated from the
checkpoint-deferred halt to the V0 halt position.
A frame-local compute exceed reports as a revert, which the per-opcode layering
carries past the detention tail rather than returning on, so the cap is installed
even though the frame is about to unwind; a TX-level exceed reports as an
out-of-gas halt, which that layering short-circuits on. The volatile checkpoint
handlers now reproduce both arms when recording their own body, instead of
returning on either.

Adds a REX6/REX7 parity test for a volatile checkpoint whose own body crosses the
compute limit, covering the halt, the recorded usage and the resulting detained
limit together.
Record REX7 checkpoint settlement and V0 gas-clamp enforcement on the
upgrade page, gate matching rules under details on compute-gas and
related metering pages, and update AGENTS.md protocol wording.
…e-break

Document that per-opcode enforcement (through Rex6) reports actual > limit
while gas-clamp enforcement (Rex7+) reports actual ≤ limit on compute and
detention halts. Normatively state that equal frame and TX remaining headroom
binds the clamp to the TX level (halt + rescue), unlike Rex6's frame-local
revert classification at the top frame.
The clamp used a zero hidden amount as the sentinel for "no clamp", which
also happens to be what an exactly-equal clamp hides. A segment whose true
remaining matched the compute headroom therefore enforced the limit but was
never reclassified: the crossing opcode's ordinary out-of-gas propagated as
an EVM out-of-gas, with no gas rescue and no MegaLimitExceeded payload.

Record the clamp as state instead — present exactly while it binds, carrying
the constraint it was bound to — so the equal case reclassifies like every
other clamp, and a segment whose own gas runs out first records no clamp at
all and keeps the EVM's own out-of-gas.
The frame-exit settlement read the interpreter's counter, and the
interpreter zeroes that counter only for a plain out-of-gas. Memory OOG,
stack underflow/overflow, invalid jump and unknown opcode all keep their
loop-exit reading and have their remainder burned later by the frame-return
rules, so the settlement saw almost none of it: a transaction that burned
its whole million-gas envelope on a memory OOG reported 21,009 compute gas,
and that figure feeds the block-level compute accounting.

Drive the settlement off the halt classification instead, and cover the
whole remainder the frame still held at the last checkpoint, including gas
the V0 clamp was hiding from the interpreter.

The burn is recorded outside limit enforcement. It is gas the EVM destroyed
rather than work the network performed, and it is bounded by the sender's
gas envelope rather than by the compute limit, so enforcing it would turn an
ordinary EVM halt into a resource-limit failure with the remaining gas
rescued — changing a receipt the carve-out requires to stay identical. No
enforcement is lost: the executed part of an exceptionally halted frame's
tail is bounded by the clamp or by a frame gas remainder that was already
under the headroom.
A clamp bound to a sub-frame's compute budget latched the transaction-level
limit into the exceed. The frame-local revert then carried that number in
its MegaLimitExceeded payload, where the calling contract can decode it and
branch on it: the same nested call that reverts with limit=956851 under
per-opcode enforcement reverted with limit=1000000 under the clamp.

Carry the binding constraint's own limit on the clamp and latch that, so
both paths report the budget that actually stopped execution.
The clamp exceed is latched at the frame's final result, and the frame-exit
settlement that closes the partial plain segment runs after it. The latch is
sticky, so the halt reason kept the pre-settlement snapshot: a transaction
ending on 21,500 compute gas reported ComputeGasLimitExceeded.actual =
21,000.

Re-read the usage from the tracker once the settlement has closed, which is
what the detention path already effectively does by rebuilding its reason
from live usage.
The helper's contract said the two runs must be indistinguishable, and the
precision invariant names state explicitly, but the assertion never looked
at it: two specs producing the same result and the same usage from different
account or storage state passed.

Compare a normalised view — account info, code, status flags, and each
slot's original/present pair. Raw EvmState carries journal bookkeeping
(`transaction_id`, per-slot `is_cold`) that identical runs can legitimately
differ on.
The exceptional-halt carve-out was written around the interpreter zeroing
its own gas counter, which it does only for ordinary out-of-gas, and said
nothing about whether the burned remainder enforces. State the rule by halt
classification, and state that the burn is reported but never evaluated
against a limit.

The clamp section now says when the clamp is in force — an exact equality
binds and hides nothing — and pins the two fields a clamp-induced exceed
reports: the binding constraint's own limit, and the transaction's final
compute usage rather than a pre-settlement snapshot.
An exceptional halt settled its whole open segment plus the clamp-hidden
gas into the non-enforcing lane, so the opcodes the frame had already
run stopped counting against the parent frame and the transaction. Code
that keeps executing after absorbing the failure could then spend the
same compute headroom a second time.

Split the settlement in two: the executed tail settles through the
ordinary enforcing path at frame exit, and only the remainder the frame
destroys goes to the non-enforcing lane. The destroyed part is read
from the frame's final result after action processing, which is also
the first point the classification is final -- revm's create-return can
still turn a successful constructor into a code-deposit out-of-gas, an
EIP-3541 reject or a runtime code-size reject.

The reported total is unchanged for every shape that was already
correct; what moves is which half of it enforces.
A checkpoint body charges its storage gas before running the raw
opcode and subtracts it back out when it records its own compute
window. A body that halts in between -- LOG in a static frame,
SELFDESTRUCT whose inner instruction runs out of gas -- never reaches
that subtraction, so the frame-exit settlement reported the charge as
compute gas.

Exclude the charge from the open segment as it is made, at every site
that debits MegaETH storage gas from inside a checkpoint body. The
normal path re-syncs the segment right afterwards, so nothing changes
there.
The KeylessDeploy sandbox exported one compute total, whose REX7
reading already includes the remainders its exceptionally halted frames
destroyed. The parent merged that as ordinary usage and then ran a
post-merge limit check, so a burn that the sandbox itself never
enforced became enforcing the moment it crossed the boundary -- turning
a constructor's ordinary EVM halt into an outer ComputeGasLimitExceeded
with the gas rescued.

Carry the split across in SandboxUsage and merge the two lanes
separately, so the parent reports the sandbox's whole total and
enforces only the part the sandbox performed.
The clamp stops the crossing opcode before it executes, so the usage
being enforced stays at or below the limit -- but the reported actual
is the transaction's full total, which also carries the remainders of
any frame that halted exceptionally earlier. Those are reported and
never enforced, so actual can be larger than limit.
`inspector_common.rs` holds what a rewriting-inspector test needs on top of the
shared driver: the REX7 limits, the bytecode shapes an inspector reaches into
(a call it can widen, a creation it can revive, a checkpoint-free loop it can
inject into), the one-lane ledgers a test asserts against, and the two ways a
refused rewrite surfaces.

`measured_inspector.rs` is the first to use it: 856 -> 584 lines.

Assertions 60 -> 53 (0 removed, 7 deduped): its `Reading`, `read` and
`assert_identity` are the shared `Outcome`, `drive` and terminal identity, its
halt-reason panic is `Outcome::halt_reason`, and the two halves of its refusal
check are `assert_refused`.
Each of `interception_gas`, `refund_and_state_gas`, `inspector_settlement_window`
and `ledger_blind_spots` carried its own reading type, driver and copy of the
conservation identity, plus its own spelling of the two bytecode shapes every
one of them needs: a call an inspector can widen, and a creation it can rewrite.

They now read the shared `Outcome` and drive through `common`, and the
plain-versus-cheated pair every rewrite is pinned as is one helper.

Lines 2951 -> 2649. Assertions 194 -> 187 (0 removed, 7 deduped): three copies
of the identity's three checks and four of the ledger equality, all of which the
driver now runs on every transaction rather than on the ones that asked.
`try_drive` is `drive` for a run the shim may refuse: it reads the tracker
before deciding, so a refused run reports the refusals counted and a surviving
one still checks the terminal identity. `drive` and `transact_inspected_refused`
are now both stated over it.

`trusted_observer` keeps its own field-by-field comparison (it compares raw
state, which the shared one does not) and loses everything else; 300 -> 215.
`frame_init_result_rewrite` keeps its own reading (it is the one file whose
runs may produce no receipt) and loses its context, its EVM and its ledger
read; 435 -> 413.

Assertions 33 -> 33.
Three things were written out that a machine can hold instead.

The two axes listed their own variants a second time in an `ALL` array, so a
variant added without touching the array would have shrunk the exhaustive sweep
silently; `axis!` derives the list from the declaration.

Every cell was a five-argument `push` spread over seven lines, of which two
arguments were the same value in all but eleven cells; `cell!` takes the three
that vary and one line per cell.

The reading, driver and identity are the shared ones, and the state comparison
is now `state_view` rather than the file's own rendering — strictly more fields,
including the deployed code and the status flags.

1851 -> 1448 lines, 66 -> 60 assertions (0 removed, 6 deduped). The grid is
unchanged and provably so: 91 covered pairs and 197 excused ones still sum to
the 288 the two axes span, with no pair both covered and excused.
Nine files became five. `shim_measurement.rs` is the five that pin what the
shim *books* — the counter and envelope edits, the two settlement windows, the
blind spots an all-zero ledger used to admit, the receipt's refund and EIP-8037
dimensions, and the gas a synthetic outcome carries — in that order, each behind
its own banner and keeping its own header prose. `shim_refusals.rs` is the two
that pin what the shim *refuses*, which had been split across three files by
which callback catches the rewrite rather than by what the rewrite is.

Three constants collided. `ACTION_DELTA` and `REFUND` held the same value in the
two files that declared each, so one declaration survives; the two `FORWARDED`s
did not, so the settlement window's is `PROBE_GAS`.

Assertions 1051 -> 1051, and the suite still runs the same 1830 test names.
…e two refusals once

The five `book_*` helpers each took a whole `MegaContext` and reached back
through it for the tracker, so a live-interpreter callback took four `RefCell`
borrows where the readings it books are one boundary, and each helper was
instantiated once per `(DB, ExtEnvs)` pair for no reason. They take the tracker
directly now: one borrow per callback, one machine copy each, and one hop less
between the boundary that measures and the lane that records.

The two refusals were the same body twice — the REX7 gate, the restore, the
count, the error slot — differing in which rewrite they recognise and how loud
a debug build is about it. `Forbidden` names those two differences and
`reject_forbidden_rewrite` is the body.

Both debug assertions are preserved, each on its own arm.
The enum, the list every sweep iterates and the label reports print were three
lists of the same twenty-eight shapes, kept in step by hand. `shapes!` takes one
row per shape and derives all three, so a shape added to the declaration cannot
shrink the sweep or print a wrong label.

The three callback pools stay written out: they are membership facts rather than
views of a row, and their order is what the draw sequence depends on.

The four live-interpreter callbacks shared a body verbatim; it is `hit_live`.
…per file

Six helpers were copied verbatim across the suite, one per file that needed
them: the Nick's-Method keyless transaction (six copies, differing only in gas
limit and init code), the checkpoint-free countdown loop (five), the two runtime
limits a compute or detention case runs under (four and three), and the
do-nothing contract with the storage-overhead reading taken off it (two each).

`common.rs` holds all six now, and `Outcome::storage_overhead` is the reading.

Assertions 1051 -> 1051. Lines 17755 -> 17540 across the suite.
`shim_measurement.rs` had grown to 3,173 lines across five banner-separated
sections, which is more than a reviewer can hold at once. Split it by mechanism,
with no test body changed:

- `shim_lanes.rs` — what each lane books: gas written into an interpreter's
  counter or a frame's gas limit, and the receipt's two other numbers.
- `shim_settlement.rs` — where a rewrite is settled when the shim's reading and
  the envelope's number are not the same object: the two settlement windows, and
  interception.
- `shim_blind_spots.rs` — the rewrite shapes an all-zero ledger used to admit.

The two magnitudes that two of the three modules edit by, `ACTION_DELTA` and
`REFUND`, move to `inspector_common.rs` rather than being copied.
…their moved lines

check_limit became check_limit_on at line 178 and current_call_remaining moved to
line 205 when the frame-limit view was introduced; the three line-anchored entries
stopped matching any generated mutant.

Claude-Session: https://claude.ai/code/session_01VNYQWJ34yvw7EV74NEZZuP

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 48907a02f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/mega-evm/src/block/executor.rs
Comment thread crates/mega-state-test/src/chaos.rs Outdated
Comment thread docs/spec/evm/compute-gas.md Outdated
Comment thread docs/spec/evm/compute-gas.md Outdated
…uppress its two equivalent gate mutants

The diff-scoped spec-gate run left three survivors on the shared frame-exit
body, all of them the MINI_REX gate shifted to EQUIVALENCE.

The `finalize_frame` gate is a real gap: under EQUIVALENCE the settlement does
not run, so an inspector's edit to a frame result's gas reaches the receipt with
the ledger's result lane — and the block guard behind it — reading it as
untouched. Two tests pin that, together with its other half: what the
measurement shim books at its own callback boundaries is unaffected, because the
shim is not spec-gated.

The other two are equivalent and recorded as such. The `gas_remaining_before`
capture has one consumer, itself behind an unmutated MINI_REX gate. The
uninspected `frame_init` reaches `finalize_frame` with a zero inspector delta,
where every branch is either REX7-gated, structurally unreachable, or writes
state no pre-MINI_REX reader can see.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56836fccc0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/state-test/src/main.rs Outdated
Comment thread crates/state-test/tests/cli_exit.rs Outdated
…an empty ledger

An empty `InspectorLedger` was the canonical block path's admission criterion, and it
cannot be: the measurement shim compares what it is handed across a callback boundary,
so an inspector that edits the interpreter's stack or memory contents, or writes the
journal directly, changes what the transaction produces and leaves every lane at zero.

The path now takes an EVM running no inspector, or one whose type its author declared
`TrustedObserver`, and refuses everything else with a new
`MegaBlockExecutionError::UndeclaredInspector` — before the transaction runs, so an
undeclared inspector's callbacks never reach the executor's state cache either. Live in
release builds, and it fails the block rather than the process. The signal travels on
`MegaTransactionOutcome::undeclared_inspector`, so the commit funnel can refuse a result
produced by another executor instance or by an embedder driving the EVM itself.

`InspectorAdjustedAccounting` stays as the backstop behind it, read at the same three
entries: a declaration that did not hold, and a result arriving at the commit funnel
already carrying a rewrite.

`MegaBlockExecutorFactory::create_executor_with_trusted_inspector` is the entry a node
tracing block production takes; `create_executor`'s `debug_assert!` forbidding a declared
observer is gone, since a declared observer is now what that path wants.
`bin/mega-evme`'s replay command carries the forwarding newtype a foreign tracer needs.
The per-vector seed hashed the fixture's full path, so the same corpus checked out under
a different root, reached through a different `--corpus-dir`, or copied into the private
fallback directory produced different seeds for identical fixtures — and a fixture pulled
out for triage on its own shares no path prefix with the one the sweep walked. A reported
seed that stops reproducing its own failure is worse than no seed.

The identity is now the fixture's file name, the unit name and the vector indexes, none of
which depends on where the corpus sits. Two fixtures with the same file name in different
directories share a seed prefix; that is not a defect, because a seed selects a mutation
stream and uniqueness is not what the identity is for.

Existing seeds change, so the per-class counts of a given global seed change with them.
…s claim

The Rex7 checkpoint section stated that every non-opcode recording site on the page is
unchanged, which contradicts the Rex7 rules stated earlier on the same page: the
code-deposit amount is weighed against the compute budgets before it is recorded, and
nothing is recorded when it does not fit or when the frame had already failed. An
implementation following the universal sentence would keep Rex6's unconditional recording
and report compute gas Rex7 deliberately omits.

The claim now names the three sites that really are unchanged and points the fourth at
the section that governs it.
…t envelope's difference

The rule said a deposit rejected before it ran anything consumes no compute capacity at
all, contradicting the sentence above it and the implementation: validation records the
standard-EVM share of intrinsic gas before it returns the error, so the transaction
enforces that amount and the envelope rebuild adds only the remainder as destroyed gas. An
implementation following the sentence would enforce zero.

The zero-capacity statement now covers the difference the rebuild introduces, and the rule
says what each shape does enforce — which on both shapes is exactly what Rex6 records for
the same transaction.

Verified against the reject and halt shapes in tests/rex7/deposit_receipt_rewrite.rs,
which pin the enforced total to Rex6's recorded total on both.
…nsaction

`create_executor_with_inspector` built an executor whose every transaction the
canonical path then refused, and stayed only because the EVM under it was
reachable through `evm_mut()`. A constructor whose product always fails is a
trap rather than an entry, so it is gone.

An undeclared inspector now reaches an executor the way a node already builds
one: `evm_factory().create_evm(db, env).with_inspector(x)` handed to the
`BlockExecutorFactory` trait entry. The tests that needed the refusal take that
route, which is the shape `mega-reth` uses and so worth exercising directly;
the ones that needed admission take `create_executor_with_trusted_inspector`.
…he runtime flag

Two rows of the construction-by-entry table admitted a transaction an inspector
had taken part in.

`inspect_transaction` runs the inspecting loop whatever `inspect` says, but
derived its answer from `has_undeclared_inspector`, which reads that flag. An
EVM whose flag was turned off through `Evm::set_inspector_enabled` and then
driven through this entry ran its inspector and reported none, and the commit
funnel let the result through. It now reads the declaration off the inspector's
type alone; `execute_transaction`, which picks its loop on the flag, keeps
reading the flag.

The shim `MegaEvm::new` and `without_inspector` build wrapped `NoOpInspector`
undeclared. Since `Evm::set_inspector_enabled` is a public trait method, an EVM
built with no inspector could have its shim switched on and then be refused for
observing nothing. Both now build the declared shim, on `NoOpInspector`'s own
declaration. `InspectEvm::set_inspector` still drops the declaration, which is
the safe direction and is now pinned.
…ng newtype per embedder

Declaring a foreign tracer read-only meant writing about a hundred lines of
per-callback forwarding, and this repository already had three copies of it —
`mega-evme`'s replay command, the tracer bench subject, and the block-executor
guard tests. Every embedder would have written a fourth.

The copies were also a quiet failure waiting: every `Inspector` method has a
default body, so a callback revm adds and a forwarder misses is not a compile
error but a callback the wrapped tracer stops receiving, and a trace short a
frame does not announce itself.

`DeclaredObserver<I>` is that forwarder, supplied once next to `TrustedObserver`
and forwarding all twelve of revm 40's callbacks. It does not weaken what a
declaration means: `DeclaredObserver(tracer)` is still an assertion someone
makes in source about one concrete inspector, moved from a newtype's definition
to the line that wraps the value, and a false one still fails the debug
verification at the callback that breaks it.

The three copies are replaced by it. `tests/block_executor/declared_observer.rs`
holds the completeness: each callback invoked directly and checked to arrive,
the callback sequence a recorder sees compared wrapped against bare, and the
same comparison against `revm-inspectors`' own tracer — the last being the one
that goes red on an upgrade, since a tracer that grows a callback the wrapper
has not grown produces a different trace.
…heir memo cells

`create_inputs_rewritten` compared the whole of `CreateInputs` with the derived
equality, and revm 40's `CreateInputs` carries two `OnceCell` memos — the address
the creation will occupy and the hash of its init code — that are filled on demand
through a shared reference. Asking a creation where it will land is what
`created_address` is for, and it is what every `revm-inspectors` tracer does at
every CREATE, so the comparison read the most ordinary thing an observation-only
tracer does as a rewritten input.

The consequences were an intervention booked for a tracer that intervened in
nothing, and — for a tracer wearing a `TrustedObserver` declaration, which debug
builds verify by measuring anyway — a panic at the first CREATE in the
transaction. `mega-evme replay --trace` takes that path, and no offline fixture
deploys anything, so the shape reached no gate.

The comparison is now written over the six fields a creation's frame is built
from, with the gas limit left to the envelope lane as before and the two memos
excluded. What the exclusion costs is recorded where the verdict is: revm reads
the address memo when it builds the frame, so a fill made with a nonce other than
the caller's redirects the deployment, and telling that apart needs the pre-bump
nonce and the keccak the memo exists to avoid — a reading no callback boundary can
take, which puts it in the content class the declaration governs.

A call's inputs keep the derived equality, which is right only because every field
of `CallInputs` says what the frame does. Both claims are now closed in
`tests/rex7/gas_surface.rs`: every field of both structs is classified semantic,
envelope or memo against upstream's own `Debug` rendering, a semantic field needs a
case that proves an edit to it is still booked, and a memo appearing on a call's
inputs fails the test that licenses the derived equality.
`DiffTally::is_failure` already fails a run on unexplained differences,
panics, unreadable fixtures, and a corpus that judged nothing. The CLI
summary only counted the first two, so a mixed corpus exited 1 while
claiming `0 tests failed out of N`, and a skipped-only corpus claimed
`0 tests failed out of 0`.

Unexplained, panics, and file errors now share the `TestsFailed` count,
with the stdout tally listing each bucket. A run that judged nothing
uses `FixtureError` instead, matching fill and validate. Chaos mode
gets the same mapping.

EEST `--mode diff` reads `--diff-report` JSON, not this text; the
report schema is unchanged.
The crate convention requires a `test_` prefix on `#[test]` functions.
Rename all thirteen exit-contract tests in `cli_exit.rs`, including the
three that predated the differential and fill coverage, so the file is
consistent.
… rendering

Closes the surviving mutants in `crates/mega-evm/src/limit/`:

- `check_limit_after_pop` on the compute-gas, KV-update and state-growth trackers had no
  test that made the pre-merge reading differ from the live one, so returning the default
  `WithinLimit` went unnoticed. Each dimension now has a late frame-local exceed: a frame
  that stayed inside its own budget and pushes its caller past theirs once merged, with the
  revert case asserted alongside for the two discardable dimensions.
- `record_inspector_gas_adjustment` closes a measured segment only where one is open; an
  adjustment taken at `initialize_interp` must book its lane and settle nothing.
- `record_inspector_action_counter_adjustment` had no assertion on the lane it books, so
  the whole body could be dropped. The new case also pins that two cancelling edits still
  leave the lane non-zero for the block guard.
- The code-deposit settlement blamed an undetained transaction-level exceed on detention
  when the detained limit had never been lowered.
- `ConservationTerms`'s `Display` renders into every assertion message the law raises, and
  nothing asserted its text.
…and the EIP-8037 mirror

Closes the surviving mutants in `crates/mega-evm/src/evm/`:

- Both transaction-level tripwires could be emptied out with no test noticing. They now run
  against a receipt whose lanes account for none of its envelope, and against a declared
  `TrustedObserver` whose ledger is not empty. The sandbox case is asserted separately,
  because the skip is what keeps a sandbox transaction out of a law stated over its parent's
  envelope.
- `EvmTr::frame_stack` is an accessor revm's `execution_result` and `catch_error` clear the
  EVM's own stack through; nothing pinned that it is not a factory.
- The EIP-8037 branch of the create classification is mirrored from upstream for lockstep
  rather than for reach, and no `MegaEVM` transaction can enter it. The classification is
  now driven directly under a configuration that enables and prices the split, so the hash
  charge, the state-gas charge and their two failure paths are all exercised.
- A call frame's outcome carries the new-account state-gas flag its inputs were built with.
- The frame-input comparison's empty-variant pair, the frame-init origin question in both
  directions, and the shim's log delegation each had no test.
- `classify_create_return`'s `state_gas_for_code > 0` guard: `>= 0` is always true for a
  `u64`, and the extra `record_state_cost(0)` it then evaluates is a total no-op that
  returns true, so the same branch is taken with nothing moved. The `==` and `<` siblings
  at the same site do skip a charge that is owed and are killed by the new tests.
- The two `if hide > 0` clamp guards, for the same reason: `record_regular_cost(0)` leaves
  the counter where it was, and the baseline sync the mutant re-runs re-writes the value
  already assigned a line above it.
- Three predicates whose mutated expression contains `cfg!(debug_assertions)`. Every build
  the suite compiles has assertions on, which makes each mutant the same constant as the
  original on every input a test can produce. The justifications state that scope rather
  than claiming full equivalence, and name what guards the assertions-off semantics.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 230d2b1478

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/mega-state-test/src/diff.rs Outdated
Stage two of the differential classifier re-ran both specs under the frame
inspector and judged the difference on the inspected pair alone. An inspector
that moved either execution could therefore explain a difference the plain runs
never produced: an observation-path regression that adds an inner exceptional
halt on the target, with the compute-gas difference intact, turns the original
unexplained difference into EXPLAINED and clears the nightly gate.

Keep the plain outcomes and require each rerun to reproduce its own plain run
first, over every quantity of the outcome but the frames the rerun exists to
collect -- `compare`'s ten plus the evidence `judge` reads directly, which a
rerun could move while leaving the compared ten alone. Any movement discards
the frame evidence, names the side and the quantities that moved, and leaves
the plain verdict standing. The "inspected pair agreed" branch is dropped: two
reruns that each reproduced their plain outcome disagree on exactly the
quantities the plain pair did, which a debug assertion now states.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api:breaking Crate interface change — downstream users must update comp:core Changes to the `mega-evm` core crate comp:doc Changes in the documentation spec:unstable Changes to the unstable spec (currently REX5)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants