feat(rex7): checkpoint compute gas settlement with gas clamp enforcement - #367
feat(rex7): checkpoint compute gas settlement with gas clamp enforcement#367RealiCZ wants to merge 89 commits into
Conversation
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.
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.
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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".
…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.
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.
There was a problem hiding this comment.
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 theBlobInvalidInputLengthdoorway 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).
|
Reviewed as a targeted pass over the conservation-law derivation ( Genuinely good:
Should fix1. The per-transaction cfg check panics, and that is too deep a place to panic.
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 2. A negative derivation is only silently saturated in release. The 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 |
A frame init that hands back a result instead of a frame carries the whole child budget as remaining gas, and the classification decides its fate: a success or revert is erased back into the caller's counter, an exceptional halt is not. The child never runs, so the frame-exit settlement never sees the halting shapes and nothing booked them — a CREATE onto an occupied address left its swallowed budget out of the reported compute total and out of block-level compute accounting, and tripped the conservation cross-check in debug builds. Settle it in after_frame_init, on the halt classification only. Precompile results are excluded: they arrive through the same arm but have already booked both halves at their own recording site. The settlement runs after the gas rescue so it reads a refreshed latch — a latched exceed means the envelope is rescued or reverted back to the caller, not destroyed.
The CALL-family volatile wrapper carries a failing body to its tail so the detention cap is applied on every path out, and then ran the checkpoint epilogue there unconditionally. revm charges the value-transfer surcharge and the argument / return-range memory expansion inside the body, before the target load and the forwarding charge that can run out of gas, so a halting body leaves those charges in the open segment with no body window left to record them. Re-opening the segment at the current counter dropped them from the frame-exit settlement about to close it: they were neither enforced as work nor booked as destroyed, and the transaction's reported compute total no longer covered the envelope it burnt. Run the epilogue only when the handler returned normally. Every `Err` stops the interpreter loop — a halt ends the frame, and the suspension that publishes a child frame is re-clamped on resume — so a clamp is never applied to a frame that is already ending. All 83 EEST state-test cases that tripped the conservation assertion under Rex7 came from this one site. The amount dropped is whatever the body had charged before it failed: 3 to 24 gas of memory expansion on its own, the 9,000 value transfer surcharge on its own, and up to 18,460 gas for the two together.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
…nherits from A state-test fixture pins what a transaction must produce, but only for a spec someone has already computed an expectation for. Rex7 has none, so sweeping the Ethereum corpus under it could only check that nothing crashed. Rex7 states the conditions under which it may *not* differ from Rex6. Read as a contrapositive, that sentence is a classifier: a difference must come with evidence, read off the execution itself, that one of the invariant's three hypotheses does not hold. `diff.rs` executes each fixture under both specs, compares the quantities the invariant names, and demands that evidence — never a list of fixtures allowed to differ. Most of the evidence is on the transaction's own result, but not all of it: an inner frame that halted and was absorbed by its caller leaves no trace there, and there are thousands of those in the corpus. Those cases take a second pass under a read-only inspector that sees every frame the EVM finished. Also lets a corpus driver keep going per unit. A fill that aborts its whole file at the first failing unit is why sweeping the corpus meant splitting every multi-unit fixture into one file per unit first; a panic that takes down a worker is why it meant one process per unit.
`--bench-spec Rex7 --diff-spec Rex6` runs the differential sweep and prints the tally, the mechanism distribution over explained differences, and every unit that needs a human. It exits non-zero on exactly two conditions — a fixture that panicked, and a difference nothing licenses — so a corpus whose declined fixtures number in the thousands still gives CI a meaningful gate. `--diff-spec` requires `--bench-spec`: a differential run compares two named specs, and taking the target from each fixture's own `post` would make the comparison mean something different from one unit to the next. `--keep-going` now also applies to `--fill`, which reports a per-unit tally instead of stopping at the first unit it cannot fill.
One command fetches the hash-pinned fixture release, unpacks its state_tests subtree, and runs every fixture under Rex7 and Rex6, comparing the two. The nightly workflow runs the same command; it needs no secrets, caches the corpus on its content hash, and builds with the `hivetests` profile — optimized, but with debug assertions live, so the Rex7 gas-conservation cross-checks actually run. Two hard gates: a fixture that panicked, and a difference nothing licenses. Coverage drift against the committed baseline is a warning in the job summary, not a red run: it means the corpus or the runner changed what it reaches, which is worth seeing but is not a defect.
The classifier decides whether a Rex7/Rex6 difference is licensed, so what it
accepts as evidence is the whole gate. Seven ways in, closed.
The fixture is the input under test, and two of the observations came out of
revert-payload bytes: a contract that wrote `MegaLimitExceeded`'s four bytes
claimed a crossed resource limit, which licenses a difference on *every*
compared quantity. Observations now carry a `Provenance`, and only ones read
off the execution — the EVM's verdict on a frame, the typed halt reason, a
tracker's own counter — can falsify a hypothesis. That leaves the
`disableVolatileDataAccess` hypothesis with no producer, since telling a guard
rejection from a contract writing the same bytes needs a latch the runner
cannot read without changing the execution; such a difference is now reported
rather than licensed.
The destroyed compute-gas remainder is derived, from a conservation law over
the transaction's envelope, not observed. A missing term in that law yields a
non-zero remainder with no halt behind it — so licensing the difference it
causes made that defect its own alibi. It now needs the halted frame the
inspector finds. Over the corpus this moves nothing but the evidence: all
17363 explained differences now rest on a frame the EVM actually finished,
where 11379 did before.
A halt was classified by whether its `Debug` rendering started with `Base`,
which handed `SystemTxInvalidCallee` and every variant a later spec adds the
standing of a crossed limit. It is matched variant by variant now, with no
catch-all arm.
The invariant that licenses a difference is Rex7's and names Rex6, so
`DiffSpecs::new` accepts that pair and refuses the rest instead of judging them
by a licence they were never granted; the workflow drops its spec inputs.
A unit is a family of transactions, one per vector its `post` names.
`TestUnit::vectors` enumerates them and diff, fill and bench each run all of
them — `--fill --force` was collapsing the `post` map to a single `{0,0,0}`
entry, deleting the other vectors' expectations. v5.4 has no multi-vector unit,
so the corpus tally is unchanged.
Finally, two ways to pass by reaching nothing. The fixture walk dropped entries
it could not read, turning a permission error into a smaller corpus and a green
run; they are reported and fail the gate. And an empty tally is truthful and
meaningless, so every mode now fails when it judged no unit, as does `run.sh`.
Corpus extraction lands via a scratch directory and a hash stamp written last,
so an interrupted unpack cannot be swept as a whole tree.
…rpus is whole A gate is only as good as what it counts and what it reads. `DiffSpecs::new` decides which spec pair the classifier may judge, and its fields were public: the pair it refuses could be assembled directly and handed to the classifier, which cannot tell one origin from the other. The fields are private now, behind accessors, and the property that the constructor is the only way in is pinned where it is true — from outside the crate, by a `compile_fail` example and by the integration tests. Three ways to pass by counting the wrong thing. Validation counted units walked, so a unit whose `post` is empty — nothing to check, nothing checked — made a file of them a passing run; it counts expectations judged. `--fill` gated its zero tally behind `--keep-going`, so the plain form exited 0 over a corpus that never arrived; the gate now covers both. And a fill counted units where every other mode counts transaction vectors, which over a multi-vector fixture makes the two sweeps' totals incomparable — `FillReport` reports one entry per vector, named the way the differential sweep names it. On the pinned corpus units and vectors coincide, so the tally is unchanged: 44023 either way, with fill's OK=36974 and ERR=7049 landing exactly on diff's PASS+EXPLAINED and SKIPPED. `--fill --force` also cleared a multi-vector unit's `out` unconditionally, dropping an expectation the fixture was entitled to. It is kept when every vector produces the same output, and the unit is refused when they differ, since one field cannot state a per-vector expectation. A unit is filled as a whole, so a vector that fails leaves the unit untouched and every one of its vectors says so. Finally, what the sweep reads. A cached corpus tree was trusted on a stamp naming the archive, which says only that some extraction of it once finished — a tree edited, truncated by a full disk, or restored intact from a cache archived mid-write still carries the stamp and still sweeps clean over a fraction of what the tally claims. The unpack now records a manifest of every file it extracted with that file's hash, and each run re-derives it from the bytes on disk before sweeping; anything missing, added or edited discards the tree. Unpacking is serialized by an atomic `mkdir` lock, and a run that cannot get it waits, then falls back to a private tree rather than writing where another process may be. `tools/eest-sweep/tests/cache_integrity.sh` drives all of that against a synthetic archive, per-PR in CI.
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. Theinterpreter_hotloopbenchmark 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
GASreadings, 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 asbaseline − remaining, hand the clamp-hidden gas back so the body runs on the true counter, re-open the window — and closes withcheckpoint_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 withMegaLimitExceededcarrying the frame's own budget, transaction-level becomes anOutOfGashalt with gas rescue, and a detained limit becomesVolatileDataAccessOutOfGaswith 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_destroyedis not the sum of the sites that destroyed it: it is derived once per transaction at settlement asdestroyed = tx_gas_spent + minted_call_stipend − non_compute_gas − enforced_compute, whereminted_call_stipendcounts the 2,300 revm mints into a value-transferringCALL/CALLCODEchild 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 adebug_assertcross-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 newcompute_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
FailedDepositreceipt 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
MegaLimitExceededrevert 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 sourcereturn_createdebits.The gas schedule is owned by the spec.
CfgEnv::gas_paramsis not an embedder surface on MegaETH: every construction and adoption path, plus a per-transaction check that also covers livemodify_cfgmutation, rejects a schedule that deviates from the spec-defined table with a loud panic naming the first differing entry — and rejects aCfgEnv::specthat 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.
AdditionalLimitlatches spec-derived flags at construction, andMegaContext::with_cfgused 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-densebench_subcallmicrobenchmarks 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)
MegaTransactionOutcomegainscompute_gas_destroyed: u64(reported statistic, derived from the conservation law) andcompute_gas_enforced: u64(the number the transaction's own enforcement ran on).BlockLimitersplits compute gas into two counters:block_compute_gas_used(full reported total, semantics unchanged) and the newblock_compute_gas_enforced(the counter block admission compares).BlockLimiter::post_execution_update_rawtakescompute_gas_enforcedas a new parameter (8 → 9 arguments); block admission accumulates it directly rather than reconstructing it by subtraction.sandbox::SandboxUsage { usage: LimitUsage, burned_compute_gas: u64 };SandboxOutcome::Completed.limit_usagechanges type accordingly.MegaBlockLimitExceededError::ComputeGasLimit.block_usednow reports the enforced reading — the counter that was actually compared.with_cfg/with_cfg_unpinned/new_with_contextand the per-transaction entry now panic on aCfgEnvwhosegas_paramsdeviate from the spec-defined schedule, or whosespecdisagrees with the context's spec. The previously carried ability to install a custom gas schedule throughCfgEnv::gas_paramsis 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_usedis the reported statistic and carries destroyed remainders.Deliberate deviations from Rex6
Each of these is documented in
docs/spec/upgrades/rex7.md:actualmay exceedlimit. Theactualon a compute-gas halt reason is the transaction's full reported total, which carries destroyed remainders that were never enforced.compute_gas_usedcovers the receipt. Rex6 has no destroyed lane and keeps its frozen accounting. Enforcement and the receipt itself are unchanged on both.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 bytests/block_executor/compute_gas_lanes.rs.cargo test -p mega-evmis green across all 14 test binaries. Spec-migration parity with direct construction (with_cfgin both directions and both builder orders) is pinned by regression tests incrates/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 Rex6executes 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.0fixtures_stable, 44,023 transaction vectors): PASS 19,611 / EXPLAINED 17,363 / UNEXPLAINED 0 / PANIC 0, with every explained difference confined tocompute_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.shpins the corpus by release and sha256, verifies the unpacked tree against a per-file manifest, andeest-nightly.ymlgates 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. TheOutOfGas-vs-MemoryOOGconvergence 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 theOutOfGaspath but restored on theMemoryOOGpath) 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.