Skip to content

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

Open
RealiCZ wants to merge 82 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 82 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.

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.

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/ (154 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. 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.

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.

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.
The interceptor answers from `frame_init`, before a child EVM frame exists, so the frame-exit settlement that splits an ordinary exceptional halt never runs for it. Two of its halts keep the whole call envelope and were leaving the unperformed part out of REX7's destroyed lane:

- the call cannot pay the fixed dispatch overhead — nothing was performed, so the whole envelope is destroyed;
- the call paid the overhead but cannot pay the deploy signer's materialization storage gas — the overhead stays enforcing, the rest is destroyed.

Both go through one helper that reads the gas before building the halt result, using the same formula frame-exit settlement uses: whatever the call still held is destroyed, whatever it recorded as compute stays enforcing.

Every other synthetic result on this path is left alone, and the reasoning is now written down where each sits: the system-contract interceptors return or revert with the envelope intact, `CallTooDeep` is a revert code, and the resource-limit and TX-level-exceed halts rescue their remainder for the sender. A rescue is a refund — recording it as destroyed too would report the same gas twice and inflate the block's compute statistic by that amount. The new test suite pins the rescued shape at zero destroyed next to the two destroying ones, and predicts each expected amount from the frame's gas envelope measured through the `GasLimitTooLow` revert rather than from the lane under test.

REX6 and earlier record nothing on any of these paths, unchanged.
@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Documentation Impact

The docs/spec/ updates on this PR remain unusually thorough (compute-gas.md, dual-gas-model.md, gas-detention.md, resource-accounting.md, resource-limits.md, hardfork-spec.md, overview.md, upgrades/rex7.md, and the top-level AGENTS.md all cover the checkpoint/clamp/burn-split/conservation-law behavior added since this comment was first posted). Two smaller spots are still missed:

Spec Documentation

Doc page Reason
docs/spec/evm/precompiles.md Its Spec History section still stops at Rex5 ("refines the KZG error-path compute-gas recording"), but Rex7 changes KZG accounting again: crates/mega-evm/src/evm/precompiles.rs now keys the halt-split arm on precompile identity (PrecompileId::KzgPointEvaluation) and halt reason (PrecompileHalt::BlobInvalidInputLength), booking the fixed KZG cost as executed and the rest of the forwarded envelope as destroyed, documented in detail under the "Precompiles" and exceptional-halt sections of docs/spec/evm/compute-gas.md (which cross-references precompiles.md for the cost override). precompiles.md should get a Rex7 line in its Spec History, or at least a pointer to the new compute-gas.md sections, so its history stays complete, consistent with how it already tracks the Rex5 change.

Agent / Skill Files

File Reason
crates/mega-evm/src/limit/AGENTS.md The STRUCTURE list still enumerates every file in limit/ (limit.rs, compute_gas.rs, data_size.rs, kv_update.rs, state_growth.rs, frame_limit.rs, storage_call_stipend.rs, mod.rs) but is missing the checkpoint.rs module added by this PR (CheckpointTracker/ClampState, REX7 checkpoint-settlement and gas-clamp state, registered in limit/mod.rs).

These updates can be included in this PR or in a follow-up.

@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: ab513f8da8

ℹ️ 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/evm/precompiles.rs
Comment thread docs/spec/overview.md Outdated
Comment thread crates/mega-evm/src/evm/instructions.rs
`MegaHandler::before_execution` answers a transaction whose initial gas outgrew its gas limit with a synthetic top-level out-of-gas that burns the whole envelope having executed nothing, produced before any frame exists — so the frame-exit settlement that splits an ordinary exceptional halt never runs for it. REX7 now takes the same split at the site: the intrinsic compute gas `validate` recorded is the only work performed and stays enforcing, and the rest of the envelope is destroyed, which makes the reported total cover what the receipt burns.

The branch is reachable on MINI_REX..REX4 only. Those specs run their initial-gas check midway through the MegaETH storage-gas additions, so a contribution added after it can still push the total past the gas limit; REX5 moved the check to the end of the additions, which turns the same transaction into a `CallGasCostMoreThanGasLimit` validation error before `pre_execution` debits the sender. No transaction on a spec that has the destroyed lane therefore reaches the halt today — the recording is what keeps the lane correct if a later spec grows an intrinsic component resolved after validation.

Both sides of that boundary are pinned. The new REX7 suite asserts that REX6 and REX7 reject the overrun in validation with the sender untouched, and that REX4 still answers it with a halt that burns the whole envelope while reporting only the intrinsic and destroying nothing. The recording itself is driven at the hook, where the REX7 split and the REX6 zero are asserted to the gas.

The synthetic result never traverses `last_frame_result`, frame-exit settlement or any rescue hook, so nothing downstream can double-count what is booked here. REX6 and earlier are unchanged.

The spec pages state the boundary rather than the site: the carve-out's enumeration of envelope-keeping halts is complete for REX7, and a validation reject — which produces no receipt — must not be recorded as a destroyed remainder.
…upling

Two gaps in the precompile seam's own coverage.

The parity matrix that holds `run_precompile_capturing_halt` to alloy-evm's `PrecompilesMap::run` left the state-gas reservoir and the call value at zero on every case. Both are forwarded fields of `PrecompileInput`, and no builtin precompile reads either, so an upstream step keyed on one of them would have passed parity unnoticed. Each now appears on a returning and on a failing case, and the matrix's dynamic precompile echoes the call value into its output the way it already echoed the reservoir into its gas — so both are observed in the compared `InterpreterResult` rather than merely handed over.

The KZG fixed-cost arm's `record_compute_gas(GAS_COST)` is safe only because it can never latch a limit exceed: a latch would make `frame_init` return a halt whose remaining gas is rescued for the sender while the same envelope was already booked as destroyed, reporting that gas twice. What rules it out is the REX5 forwarded-gas cap — the effective limit the arm tests against the fixed fee is `min(gas_limit, remaining)` for the very remaining the limit check consults — and nothing said so. A `debug_assert` now states it at the recording site, and a boundary probe pins both halves on REX5, REX6 and REX7: with the budget exactly at the fixed fee the arm fires and lands on the limit rather than over it, and one unit lower the cap sends the call into the wrapper's gas gate so the arm is not reached at all.

No behavior change on any spec; the assert compiles out of release builds.
Add a settlement-point derivation of a REX7 transaction's destroyed
compute gas from what its envelope actually spent, and cross-check it in
debug builds against the total the per-site recordings booked:

    destroyed = spent + double_counted_call_stipend
                - non_compute_gas - enforced_compute_gas

Two lanes feed it. The non-compute lane collects the EVM gas a
transaction spends that is neither compute work nor a destroyed
remainder: in-frame MegaETH storage gas (through the existing
exclude_storage_gas_from_segment funnel), the MegaETH share of intrinsic
gas, the code-deposit storage charge, the KeylessDeploy caller
materialization charge, and the sandbox boundary's residue. The stipend
lane measures the one place recorded compute gas is deliberately not a
partition of the envelope: a value-transferring CALL/CALLCODE has its
CALL_STIPEND recorded by the caller and again by the callee, so the two
frames book one stipend more than the envelope ever debited.

Both lanes are REX7-gated at the tracker and the derivation lives inside
a debug_assert, so frozen specs and release builds are unaffected.
EIP-3529 refunds and the EIP-7623 floor need no term: they are applied
after this read point and only move the number the receipt reports.

Reading the envelope with total_gas_spent instead of the deprecated
spent, which is the same subtraction while EIP-8037 stays pinned off.
…s it

The CALL_STIPEND term in the destroyed-remainder derivation is not a double
count: revm mints the stipend into the child frame's budget without debiting
the caller, and the caller's compute window already subtracts exactly what it
contributed. Rename the field and its accessors to minted_call_stipend and
restate every comment in terms of the mint.
MegaTransactionOutcome.compute_gas_destroyed now comes from the conservation
law settled once the transaction's envelope is final, not from the sum of the
sites that destroyed it. The per-site bookings stay as the enforcement split
and as the independent second opinion the settlement point's debug assert
holds the derivation to. A negative derivation clamps to zero in release and
trips in debug.
New rex7/conservation_terms module: a minted call stipend and a destroyed
envelope in one transaction, several mints in one transaction, and a
KeylessDeploy sandbox whose EIP-3529 refund drives the non-compute lane
negative (on its own and alongside a destroyed remainder). Seam tests in
limit.rs drive the negative-derivation guard and the signed lane directly.
The EIP-8037 reservoir is documented as pinned off rather than constructed.
The spec now defines a transaction's destroyed compute gas as the remainder
of what it spent — envelope plus minted call stipends, less MegaETH storage
gas and enforced compute — and demotes the site list to what fixes the
executed side at each site. The completeness claim becomes a corollary of the
law rather than an assertion about the enumeration. AGENTS.md gains the
obligation to re-run the conservation scan after a revm / alloy-evm upgrade.
The block's enforced compute counter was reconstructed as the reported total
less the transaction's derived destroyed remainder, which put a reporting
derivation on an enforcement face: in release builds a missing term in the law
would have repacked blocks rather than misreported a statistic.
MegaTransactionOutcome now carries compute_gas_enforced, read from the lane the
transaction enforced its own compute limit on, and the block accumulates that.

The call-stipend term's stated condition is corrected the other way: the spec
text and the comments said the stipend is counted once its child frame runs,
while the implementation books it per mint at the CALL-family settlement. A
value call turned away at frame entry runs no child, yet its refund returns the
mint into the caller's envelope, so the law needs it — the implementation was
right and is now pinned, the wording was wrong by 2,300 gas per such call.

Also aligns the settlement-point rationale with the before_execution
short-circuit, which produces a receipt without reaching settlement, and
rewrites rex7.md's circular executed_compute phrasing.

@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: b68a838f11

ℹ️ 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 docs/spec/evm/compute-gas.md Outdated
Comment thread docs/spec/evm/compute-gas.md Outdated
Comment thread crates/mega-evm/src/evm/precompiles.rs Outdated
Comment thread docs/spec/evm/compute-gas.md Outdated
@RealiCZ RealiCZ changed the title [wip] feat(rex7): checkpoint compute gas settlement with V0 gas clamp enforcement [wip] feat(rex7): checkpoint compute gas settlement with gas clamp enforcement Aug 17, 2026

@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: 4d3986fad4

ℹ️ 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/limit/limit.rs
…claims

One sentence per line in the roster entries, the conservation-law terms
and the clamp procedure — the `minted_stipends` term ran six sentences
on one line.

The KZG boundary's 192 becomes `KZG_POINT_EVALUATION_INPUT_LENGTH` in
the compute-gas constants table, referenced from both pages that
described the boundary with a bare number.

The exceptional-halt carve-out claimed a frame's whole budget settles as
compute gas while its own executed definition subtracts storage gas
charged before the abort. The dichotomy now covers the compute part
only, and the storage charge is stated as belonging to neither half.

The precompile mirror comment named alloy-evm 0.37.1; the workspace pins
0.36.0. It now names the pinned version and records that 0.37.1's `run`
body is byte-identical, so the upgrade-comparison obligation stands
without implying the mirror is stale.
REX7 takes the KZG fixed-fee arm only when the dispatched
precompile's id is KzgPointEvaluation. A Custom override at the
KZG address falls through to the generic halt arm. Frozen specs
keep the address-only match.
…nsaction

A REX7 transaction's reported compute total, the MegaETH storage gas it was
charged, and the CALL_STIPEND the EVM minted into its child frames are the
terms the destroyed remainder is derived from, so once settlement has run they
must add back up to the envelope the receipt reports. Nothing checked that.
The settlement site's own derived-versus-booked cross-check cannot: it is happy
whenever settlement runs, and says nothing about a settlement that never ran or
an envelope decided after it.

Assert the identity in two places. The shared REX7 test helpers now funnel every
transaction through one assembly point that checks it, which turns the whole
suite into a checker rather than only the tests written to look at gas. A
debug-only assertion at the transaction-outcome construction point extends the
same check to every corpus that runs through the crate in a debug build.

Anchoring on the pre-refund envelope is what keeps the identity correction-free:
the EIP-3529 refund and the EIP-7623 floor move the receipt's number without
anyone having burnt the difference, and both are carried as their own result
fields rather than folded into the envelope.
An OP deposit is not allowed to fail, so a deposit that does fail has its result
rebuilt: state rolls back to the sender's nonce bump and the mint, and the
receipt reports the transaction's whole gas limit. That rebuild happens at the
outermost error boundary, past every site that records or settles compute gas,
so nothing on the MegaETH side saw it. A deposit rejected in validation reported
only the standard-EVM share of its intrinsic gas against a receipt burning far
more; a deposit stopped by a per-transaction resource limit reported the
envelope its gas rescue had shrunk, while the receipt was raised back to the
full limit.

Settle the rewritten envelope at that boundary. The difference between what the
conservation law derives for the rebuilt envelope and what the per-site bookings
already hold is destroyed compute gas — the receipt burns it and nothing was
executed for it — so it goes to the non-enforcing lane and the derivation is
re-settled against the rebuilt envelope. Enforcement does not move: what the
per-transaction limits and the block's admission counter read stays exactly the
work the transaction performed, which is what keeps a deposit rejected before it
ran anything from consuming block compute capacity.

Skipped inside a keyless-deploy sandbox, whose own rejected transactions never
settle a derivation: the law is stated over an outer transaction's final
envelope, and the sandbox's gas is a charge inside its parent's.

Pre-REX7 specs have no destroyed lane and are untouched.

Also corrects the premise the old accounting rested on, in the spec pages, the
intrinsic-gas recording site, and the test module that pinned it: a validation
reject producing no receipt is true of ordinary transactions only.
@RealiCZ RealiCZ changed the title [wip] feat(rex7): checkpoint compute gas settlement with gas clamp enforcement feat(rex7): checkpoint compute gas settlement with gas clamp enforcement Aug 18, 2026
The canonical code-deposit charge is one revm only takes when the frame's
result is still successful at action processing. REX5 records it ahead of
that point so a compute exceed can fail the frame while the deployment is
still revertible, and the amount then stays in the tracker however the frame
ends -- including when it ends on a data-size or state-growth exceed, where
no deposit is charged at all. That leaves the transaction reporting, and the
block enforcing, compute gas nothing spent, which breaks the REX7 envelope
conservation law.

REX7 now settles the charge at the frame's exit, after the tail segment is
settled and the frame-exit accounting merged, and asks a non-mutating peek
whether it fits before recording it. A frame-local exceed reverts the frame
without recording or latching; a TX-level exceed latches and halts with gas
rescue, carrying the detention attribution the recorded path would have had.
REX4-REX6 keep their existing recording point and behavior.
revm's create-return debits `gas_params().code_deposit_cost(len)`, so an
embedder that installs its own gas schedule moves the amount a successful
CREATE pays. REX7's settlement read revm's built-in per-byte constant
instead, which under such a schedule made the recorded charge differ from
the debited one on every CREATE, and made the affordability predicate
answer for a charge revm was not about to take.

Take all three readings — the predicate, the weighing peek and the record —
from the configuration's schedule. REX5/REX6 keep the constant: their
behavior is frozen, and they have no conservation law behind the charge.

Under the default schedule the two readings are the same number, so nothing
about a mainnet transaction changes.
revm 40 turned every operation's price into a `CfgEnv.gas_params` table an
embedder can rewrite, but several MegaETH accounting sites carry the schedule's
values as constants: the `CALL_STIPEND` a value-transferring call mints (booked
for the destroyed-gas conservation law, and subtracted back out by the 98/100
forwarding cap), the pre-REX7 per-byte code-deposit rate, and the mainnet table
the keyless-deploy preflight estimates intrinsic gas from. Under a rewritten
table those sites book something other than what revm charged.

The gas schedule is a property of the spec, so a configuration carrying anything
other than `GasParams::new_spec(SpecId::from(cfg.spec))` is now rejected with a
panic naming the entry that deviated and both values, rather than executed. The
check runs at both `with_cfg` entry points (covering the factory, the block
executor and every tool), at the deprecated `new_with_context`, and again at the
point of use before every transaction, which also covers a configuration mutated
in place after the context was built. It is unconditional across specs: the
configuration domain has no historical block coverage to preserve.

Tests that exercised a rewritten schedule become pins that it is rejected. The
CREATE knife-edge case separating the active-schedule reading from the constant
is removed with a note in the module doc: it needed an inadmissible
configuration, so the two readings can no longer disagree on any input.
`MegaContext` carries its spec twice — the `MegaSpecId` that selects the
instruction table, precompiles and resource-limit trackers when the EVM is
built, and the `OpSpecId` in `CfgEnv` that revm's own gating reads while a
transaction runs. Rewriting `cfg.spec` on a live context through the mutable
deref leaves the two naming different forks, so one transaction executes under
two specs at once. Rewriting the gas schedule along with it keeps the schedule
pin satisfied, so that check alone does not catch the shape.

Check the two against each other in `on_new_tx`, ahead of the schedule pin.
The compute-gas page declares `spec: Rex6`, and unstable-spec behavior belongs
in a `<details>` block rather than in main prose and tables. Move the Rex7
table row and the Rex7 bullet of the code-deposit rules into one, and record
the change in the page's Rex7 spec-history entry.

@mega-maxwell mega-maxwell 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.

⚠️ Review needs attention — 2 finding(s)

0 blocking · 0 should-fix · 2 suggestion(s) · 0 open question(s)

Reviewed head 456a115a.

Findings without inline anchors:

  • docs/spec/evm/precompiles.md:60[Minor] Spec History in precompiles.md stops at Rex5, missing Rex7 KZG-identity change A reader following this page's Spec History to trace KZG override behavior will believe Rex5 is the last accounting-relevant change and miss the Rex7 identity-keyed halt split. The prior claude[bot] documentation-impact comment on this PR explicitly named this gap and left it unaddressed; the shipped half (code + compute-gas.md prose) works against a half (this page's history) that never moved, so nothing looks wrong on the diff itself. Suggested fix: Add a new bullet after line 60, e.g. - [Rex7](../upgrades/rex7.md) keys the KZG halt-split accounting on precompile identity (and the BlobInvalidInputLength doorway reject); see the Precompiles and exceptional-halt sections of compute-gas.md.
  • crates/mega-evm/src/limit/AGENTS.md:14[Minor] limit/AGENTS.md STRUCTURE list omits the newly added checkpoint.rs module Agent-facing orientation for this subsystem now silently under-describes it: a future contributor reading STRUCTURE to find the file that owns checkpoint settlement or clamp state will not see it listed. Every prior module in this directory has an entry, so the omission is easy to over-trust. Suggested fix: Add a bullet (e.g. before line 14) such as - checkpoint.rs: Rex7 checkpoint-settlement and gas-clamp state (CheckpointTracker, ClampState).

@vincent-k2026

Copy link
Copy Markdown
Contributor

Reviewed as a targeted pass over the conservation-law derivation (limit/limit.rs) and the cfg pin/panic surface (evm/context.rs) — not line-by-line across the checkpoint state machine or all 154 Rex7 tests. Rex7 being unscheduled lowers the urgency, not the value of settling the design questions now.

Genuinely good:

  • The conservation law is i128, not u64. derived_burned_compute_gas (limit/limit.rs:275) deliberately keeps the sign, and the comment is exactly right: "Signed on purpose: a mismatch against the booked total is a defect to report, and clamping it at zero would hide the half of the mismatch space where the booking over-counts." I went into this file expecting to find a saturating_sub swallowing a bug and found the opposite — saturation happens only at the final reporting step, and compute_gas_destroyed never feeds enforcement. Worth calling out specifically.
  • Keeping the per-site bookings as an independent second opinion that a debug_assert cross-checks the derivation against, rather than letting the derivation replace them, is the right redundancy.
  • Splitting compute_gas_enforced from compute_gas_used and stating in the API notes that any consumer accumulating compute usage into a further limit must use the enforced one closes the consumer trap up front.

Should fix

1. The per-transaction cfg check panics, and that is too deep a place to panic.

context.rs:972 (panic_gas_schedule_mismatch) and :1021 (panic_context_spec_mismatch) hang off with_cfg / with_cfg_unpinned / new_with_context and the per-transaction entry ("plus a per-transaction check that also covers live modify_cfg mutation").

Panicking at construction I fully agree with — that's a startup boundary, and a wrong gas schedule is a consensus fork, so the earlier it blows the better. The per-transaction one is different: at that point you are mid-block-execution, and a panic takes the whole mega-reth process down instead of failing that transaction.

Suggestion: keep the panic at construction; make the per-transaction check a debug_assert (construction already covers the static case), or return a typed MegaTransactionError. If live modify_cfg mutation really has to be caught at runtime, it should be caught as a transaction-level failure.

2. A negative derivation is only silently saturated in release. The debug_assert is the only thing that catches a site destroying an envelope without booking it, and mainnet runs release builds. A release-build counter/metric (compute_gas_derivation_negative_total) turns a debug-only invariant into an operable signal; it doesn't feed enforcement, so the cost is one counter.

3. Same as #365: the body says "Marked WIP" but the title has no WIP and the PR is not a draft. Here the WIP reason is an external question — whether SandboxUsage's shape is what mega-reth wants to consume. That needs someone on the mega-reth side to answer; worth pinging them directly, otherwise this sits open indefinitely.

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