Repository navigation
Conversation
Two defaulted `Host` methods let a consumer meter inside revm's own instructions, at the two points it otherwise reconstructs from outside: - `write_completed(write: CompletedWrite<'_>, gas: &mut GasTracker) -> WriteVerdict`, called by SSTORE, LOG0-LOG4 and SELFDESTRUCT once the write is in the journal and every charge the instruction makes has been made, so only on the instruction's success path; - `frame_starting(start: &FrameStart, gas: &mut GasTracker) -> bool`, called by CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE and CREATE2 once the forward is taken (`record_withheld_first_cost`) and before the frame's input is built, so the input copies the reservoir as the hook left it. The hook may debit and credit the running frame's tracker; the instruction uses it as the hook left it and charges nothing after the hook. One private helper applies the write verdict: `Continue` ends the instruction as before, `OutOfGas` fails it as a failed charge does, `Revert(output)` ends the frame as a `Revert` with the output on the gas the hook left (a SELFDESTRUCT so reverted does not self-destruct). A start hook's `false` fails the instruction out of gas and starts no frame. The types live in a new file, `host/instruction_hooks.rs`: `CompletedWrite` and `FrameStart` are `#[non_exhaustive]`, the latter built with `FrameStart::new`. The reported forward is the exact amount deducted from the caller: `load_acc_and_calc_gas` now wraps a crate-private `load_acc_and_calc_forward` that also returns it, last, so the call site does not repeat the value stipend's rule and no public signature changes. `load_acc_and_calc_gas` keeps its signature and results; it is `#[inline]` now, around the out-of-line helper, and nothing in the workspace calls it. The defaults continue and return `true`, touching nothing. Under callgrind (the fork's release profile) the write sites and the creations cost nothing per instruction; a call costs one instruction, the store of the returned forward. Reconstructing the forward at the call site instead costs the same, through the call instruction's stack layout.
… inspected path In the interpreter, on a host that records each hook call with the gas it was handed and answers as told: - each write instruction (a cold slot set and one cleared, a log, a cold SELFDESTRUCT into a new account) reaches the hook after its last charge, refunds included, on Berlin, Osaka and Amsterdam: the frame ends on the gas the hook was handed, and one gas short of the charges before the hook fails without reaching it; every earlier failure (the stipend sentry, the static, dynamic, memory and log charges, a refused cold target, a failed state-gas price) never reaches it; - every verdict: `Continue` and `Revert` on the gas the hook left, a reverted SELFDESTRUCT returning `Revert`, `OutOfGas` halting with the record of what the hook left, or of the hook's own failed charge; - every call kind and both creations: the start's facts, the forward already taken, nothing charged after the hook, the value stipend outside the forward, a named gas against the 63/64 cap, the input's reservoir as the hook left it for a covered and a spilled hook charge, `false` halting out of gas with no frame, and every failure before the forward (memory, value, cold target, account and state-gas charges, price lookup, a forward before EIP-150, init-code and creation charges) never reaching it. Through `transact` and `inspect_tx` on a context that overrides the hooks, each run on both paths with the same outcome and the same hook calls: observing hooks change nothing against the default context on either path, on Prague and Amsterdam, which report the gas the fork reported before the hooks; each instruction, every call kind included, one gas short never reaches its hook, and with exactly the gas reaches it with nothing left and completes; each verdict ends the transaction the same way; a refused start runs no frame; each call kind's forward is the named gas or the 63/64 cap, taken before the hook, with the stipend outside it; a covered and a spilled charge by a write hook and by a start hook stand, and a started frame, or a precompile's answered one, inherits the reservoir the hook left.
Two entries in "Hooks", in the section's format: - Completed-write hook: `Host::write_completed`, called by SSTORE, LOG0-LOG4 and SELFDESTRUCT after every charge they make, refunds included, so only on their success path; the verdicts and what each does to the frame. - Frame-start hook: `Host::frame_starting`, called by the call and creation instructions after the forward and before the input is built; what the start's facts are, and what `true` and `false` do. Both say the hook may debit and credit the tracker it is handed and the instruction uses it as the hook left it, that a charge the hook fails records as any failed charge does, and what the defaults cost where measured: nothing for the write sites and the creations, one store per call. The failed-charge record's entry and its doc comments now list a hook's out-of-gas among the halts the interpreter records.
`CompletedWrite` loses `#[non_exhaustive]`, and `FrameStartKind` stays without it. mega-evm matches both for consensus metering; a non-exhaustive enum forces it a wildcard arm, through which a kind added later would pass un-metered without a compile error. Exhaustive, a new kind is a compile error in every consumer that matches it, and adding one is a deliberate breaking change. Each enum's doc says so. `FrameStart` keeps `#[non_exhaustive]`: it is read through its fields and built with `FrameStart::new`, so a later field is not breaking. The tests' `Write::of` drops its wildcard arm, which no longer compiles cleanly, and now matches `CompletedWrite` exhaustively from outside the crate: putting the attribute back fails it with E0004.
The completed-write and frame-start entries drop `#[non_exhaustive]` for `CompletedWrite`, list `FrameStartKind`'s kinds, and say both enums are exhaustive on purpose: mega-evm matches them to meter each write and charge each start, so a new kind is a compile error in every consumer rather than a kind that passes a wildcard arm unmetered, and adding one is an `api:exception` change.
…run they wrap
The "Precompile price" entry's Coverage bullet said the port to the
op-revm fork was outstanding. That fork has done it: at v20.0.0-mega.3
its nine size-limited wrappers (bn254_pair::{GRANITE, JOVIAN, KARST} and
bls12_381::{ISTHMUS, JOVIAN}_{G1_MSM, G2_MSM, PAIRING}) each carry a
price function set with `with_required_gas`, guarded by its
`tests/required_gas.rs`.
The note becomes the rule it stood for, since this doc carries rules and
not dated history: a precompile defined outside this repository that
wraps a revm run must price itself from the run it wraps, as the op-revm
fork's size-limited wrappers do, with a pointer to that fork's
"Precompile prices" entry for their guard.
…r on GasTracker The byte pool is a budget of bytes a frame may append: bytes_remaining, unlimited by default, drawn by record_bytes, given back by refill_bytes, taken out for a child by forward_bytes and added back by absorb_returned_bytes. Its net, bytes_spent, is signed as history gas is, so rollback_state_gas gives a failing frame's draws back and a merge hands a successful frame's net on. A draw the pool cannot pay records a BytesCrossing on the failure side. The history allowance is history gas a frame may spend before it charges its budget; draw_history_allowance takes it first and returns the rest. The start charge is what the frame-start hook declares it charged the caller for the frame and the frame's failure gives back; the instruction takes it off the tracker and puts it on the frame's input. Nothing in the workspace draws any of the three, so a consumer that sets nothing sees upstream's behaviour. The tracker is 136 bytes.
…all history allowance bytes_forward_percent (id 254) is the share of a caller's byte pool a child frame receives, in per cent rounded down; a hundred on every spec, so the whole pool is forwarded. call_history_allowance (id 253) is the history gas a value call's child may spend before it charges its budget; zero on every spec. Both are fork ids allocated from the top of the table.
…story allowance to the frame input After the frame-start hook has answered, the call and creation instructions take the start charge the hook declared off the caller's gas, take the child's share of the caller's byte pool out of the pool by the schedule's bytes_forward_percent, and put both on the frame's input; a value call's input also carries the schedule's call_history_allowance, decided where the stipend is. CallInputs, CreateInputs, CallOutcome and CreateOutcome gain the fields (charged_start, bytes_share, history_allowance); the pool methods are forwarded on Gas. The defaults forward the whole pool, declare nothing and give no allowance.
…arge A frame's gas starts with the share its caller forwarded as its byte pool and with the allowance its input carries, on the frame the handler runs and on an answer it gives without one. handle_reservoir_remaining_gas settles the pool as it settles the reservoir, not as regular gas: a child's rollback gives its draws back, so the caller absorbs the child's remaining pool on success and the whole share on revert and on halt, and the child's net draw merges into the caller's on success only, so a caller's rollback undoes its children's draws too. The transaction's frame gets the whole pool and the start charge a consumer declared on the transaction's gas. The start charge on a frame's input rides to its outcome and is refunded whole by return_result, and by last_frame_result for the first frame, on the condition the upfront state charge is refunded on, with refill_history and refill_bytes.
…r, on the plain and the inspected path The handler tests set a pool on the transaction's gas and drive the frames with instructions of their own that stand in for a consumer's hooks: the forward and the settlement at depth, a failed draw's record, the start charge refunded on revert, halt, a refused start, an answered start and a failed creation, the first frame's pool and charge, the allowance set on a value call's child and drawn only by the consumer, and a consumer that sets nothing ending as the mainnet handler does. The inspector tests run the same scenarios with transact and inspect_tx and check that both paths end the same way.
…GAETH-FORK.md Three hook entries in the section's format, the frame-start hook's contract extended by what the instruction takes off the caller's gas after the hook, and the failed-charge entry's tracker size brought up to date.
…t the tracker `Host::frame_starting` takes `&mut FrameStart`, and the hook declares the refundable part of its charge in the new `FrameStart::charge`. The call and creation instructions copy it onto the frame's input when the hook admits the start; a refused start drops it with the start. The carrier on `GasTracker` (`start_charge`, `set_start_charge`, `take_start_charge`, forwarded on `Gas`) is gone, and with it 16 bytes of every tracker (136 -> 120), so every copy of `Gas`, `InterpreterResult` and a frame result moves 16 bytes less. The transaction's frame has no start hook: a consumer that charges the transaction's gas for it puts the charge on the first frame's input (`Handler::first_frame_input`), and `last_frame_result` refunds it as before. The handler and inspector tests declare through a start hook on a context of their own instead of the tracker; their figures are unchanged.
… record
The failed-charge record was an `Option<u64>`, whose tag took a word of its
own, and the byte pool's crossing a second word (`Option<BytesCrossing>`).
The record is now a number and a flag, and the crossing a `bool` in the
same word as the flag: `GasTracker` goes from 120 to 112 bytes.
`bytes_crossing` returns `bool` and `set_bytes_crossing` takes one; the
record says that a draw failed, not what it asked for, which the hook that
drew knows when the draw fails. `BytesCrossing` is gone.
`failed_charge_remaining` and its setter keep their signatures and
behaviour, and `PartialEq` and `Hash` see the same figures, since the
number is zero whenever the flag is clear.
`GasTracker` (de)serializes through a mirror (`gas/encoding.rs`) with the
same fields, names, order and defaults as the derived encoding: the
failed-charge record is still an optional number. Only the crossing's own
encoding changes, from `{"cost": n}` or `null` to a `bool`; it has not
been released.
…sizes in MEGAETH-FORK.md The frame-start hook takes `&mut FrameStart` and declares the start charge on it; the carrier on the tracker is gone. The byte pool's crossing is a bit beside the failed-charge record's flag, and `BytesCrossing` is gone. The tracker is 112 bytes and a frame result 248; the default cost is measured against the base.
…rame's tracker `ContextTr::admit_code_deposit` takes the creating frame's tracker as `gas: &mut GasTracker` beside the deposit, as `write_completed` and `frame_starting` take the running frame's. The hook may charge it: a consumer that holds deployed code to a byte budget draws the code's bytes from the frame's pool there, between `return_create`'s last charge and the checkpoint commit. An admitted deposit returns with the gas as the hook left it; a refusal puts it back to `gas_before`, which undoes the hook's charges and a crossing its failed draw recorded. `CodeDeposit` is unchanged and stays `Copy`: `gas_after` reads a copy of the frame's gas as `return_create` left it, so the deposit's charges read the same whatever the hook does. The default hook admits and touches nothing. This changes a released hook's signature, so an override of the hook does not compile until it takes the parameter (`api:exception`). mega-evm's `MegaContext` is the one override found. Tests: the hook draws on the frame's gas through `return_create` (admitted, admitted with a failed draw, refused, refused with a failed draw, and the default hook), and end to end, where an admitted deposit's draw reaches the transaction's net and a refused one gives the whole share back.
…rovider drew `make_call_frame` set a precompile answer's pool to the caller's share and kept the net the provider left, so a provider that drew on its answer returned a pool and a net that did not add up to the share: on success the caller absorbed the whole share and merged the draw on top, and on failure the rollback gave back more than the share. The answer now adopts the share as the pool it started with: `GasTracker::adopt_bytes_share(share)`, plumbing beside `forward_bytes` and `absorb_returned_bytes`, sets the pool to the share less the net drawn so far, so the pool and the net add up to the share as a frame's do and a rollback restores the share. `EthPrecompiles` draws nothing, so its answers start with the whole share as before; the derivation costs 10 instructions per precompile answer (callgrind, the fork's release profile; +0.003% of a transaction that calls a precompile). Tests: a provider that draws 40 bytes on its answer, through the handler, where the caller takes the draw over when the call succeeds and gets the whole share back when it fails; and the tracker adopting a share after draws, after a refill below zero and after none.
…tial share and inspector outcomes Pins behaviour the suites did not reach: - the transaction's frame of a creating transaction gets the whole pool and the start charge declared on its input, refunded when the creation reverts or halts and kept when it deploys (the first-frame test now runs a call's frame and a creation's); - a start the handler refuses for the call depth, a call's and a creation's, gives its caller the whole share and the start charge back; - with `bytes_forward_percent` at 98, every start through the handler forwards 98% of the caller's pool rounded down, and the transaction ends with the figures the whole-pool forward gives; - on the inspected path, an outcome the inspector builds to answer a start settles as built (`CallOutcome::new` carries no start charge and an unlimited pool; one carrying the input's share and charge settles as the frame would), and a success the inspector rewrites into a revert at `call_end` refunds the start charge and rolls the frame's draws back. Checked against mutants: dropping the creating transaction's pool forward and giving the depth refusal a plain `CallOutcome::new` each fail a test.
…he literal consumers in MEGAETH-FORK.md - Code-deposit admission: the hook takes the creating frame's tracker as `gas`; an admitted deposit returns with it as the hook left it, a refusal puts it back to `gas_before`; lending it costs nothing where measured; the signature change is an `api:exception`, with the one override found (mega-evm's `MegaContext`). - The byte pool: a consumer draws deployed code's bytes at admission; `adopt_bytes_share` in the plumbing list; the one sentence on outcomes built outside a frame becomes three cases: a precompile's answer (the share less the provider's draw), an inspector's answer (as built), a consumer's own answer (sets the share). The cost figures are re-measured at this head: a transfer 120 instructions, a precompile call 135. - The start charge: a precompile's answer and a start the handler refuses carry the input's charge; an inspector's answer keeps the caller's charge unless it copies it, as revm keeps its upfront state charge on an outcome a tool builds; an outcome rewritten at `call_end` / `create_end` keeps the frame's charge; a consumer's own answer follows the same rule. - The `api:exception` consumer list names Foundry's and mega-reth's oracle executor's literal constructions: their outcome literals already fail against the fork's base on the earlier EIP-8037 field `charged_state_gas_address`; Foundry's one `CallInputs` literal is new breakage, in a file its outcome literals already break. Also wraps a `record_bytes` doc line that ran past 100 columns.
… handler calls `execution::settle_first_frame` makes every step of mainnet's `Handler::last_frame_result` but the final copy into the frame result: the spend and the settlement of the frame into the transaction-level gas, the EIP-2780 state-gas refund with its error handling, and the start charge refund, each with its halt rule. Mainnet's `last_frame_result` calls it, so a chain that overrides the method (op-revm) calls it too rather than copying the body, and a step the fork adds reaches it. Inlined, so the mainnet handler compiles to the code it had.
…uilds The byte pool's net is an `i64` while draws and refills take `u64` amounts, as the gas nets are, but only the gas amounts had a documented bound. Document the same bound for bytes (every draw and refill, and the net with its children's merged in, within `i64`) and assert it with `debug_assert!` where the net moves: `record_bytes`, `refill_bytes` and `add_bytes_spent`. Release builds are unchanged. A test pins the boundary: a draw, a refill and a merged net of `i64::MAX` each roll back exactly, and a draw one byte past it panics in a debug build.
…rmats that carry field names A payload encoded before a field existed decodes with the field's `serde(default)` in a format that carries field names, such as JSON. A format that encodes a struct by position, such as bincode, reads the old fields in their places and then looks for the new ones, so it cannot read such a payload, as with upstream's own field additions. Every doc comment that said older encodings decode now says in which formats, on the byte pool's fields and on the fork's earlier ones (history gas, the withheld part, the failed-charge record, the charged state-gas address). `GasTracker`, the encoding mirror and the serde test modules state the positional limit, the mirror's test no longer says a positional format reads the encoding as before, and the JSON-only tests say JSON in their names.
`adopt_bytes_share` negates the net, and the negation of `i64::MIN` saturates, so a share adopted against that net would be a byte short. The bound is now `±i64::MAX` for the net: the debug asserts in `refill_bytes` and `add_bytes_spent` also refuse a net of `i64::MIN`, and a test refills one byte past `-i64::MAX` and panics in a debug build. Release builds are unchanged.
…ncodings in MEGAETH-FORK.md - The start charge: `execution::settle_first_frame` is the first frame's settlement, which a chain's handler that overrides `last_frame_result` calls (op-revm's does); measured at the same instruction count. Upstream's steps of that settlement now live in `execution/first_frame.rs`, where a pick that changes them lands. - The consumer inventory names each consumer at a revision: the op-revm fork (compiles, its next release calls the shared settlement), the legacy engine (`501cc7cc`, a production `CallInputs` literal and test literals), Satin W9 (`f1d1a48d`, production and test literals, its `frame_starting` and `admit_code_deposit` overrides), Foundry (on revm 43 at master) and mega-reth (`2dc2f44ac8`, `b97b508a0b`). The frame-start hook's `&mut FrameStart` is listed with the breaking items. - Serde: older encodings decode with the new fields' defaults only in formats that carry field names; a positional format cannot read them. - The byte pool: byte amounts are bounded as gas amounts are, the net to `±i64::MAX`, asserted in debug builds. - The history allowance: a precompile's answer carries the input's.
`FrameStart::target` was `None` for `CREATE` and `CREATE2`, so the frame-start hook could not tell whether the transaction had already changed the account a creation takes. It now carries that address. With EIP-8037 the instruction already computes the address into `CreateInputs`' cache before the hook. Without it, the instruction now reads the caller (the running frame's account, in the journal) and, after the same three checks the EIP-8037 branch makes (endowment, nonce, depth), computes the address into the same cache. The handler builds the frame from the cache, so the address and a `CREATE2`'s init-code hash are still computed once, and a creation the handler fails before it takes an address computes none and reaches the hook with `None`.
…ce before it After the write, a source loaded with 5 and destructed from 7 looks the same as one destructed from 5: both hold 0 against an original 5, and `SelfDestructResult` says only that each had a value. A consumer that counts an account write record while the account's info differs from what the transaction loaded could not tell whether the write changed the source. `CompletedWrite::SelfDestruct` now carries `balance`, the balance the destructed account held before the write, read from the running frame's own account before `Host::selfdestruct` (warm, so no cold load). The write changes only balances, so with the journal at the hook it rebuilds both accounts before the write: the source's balance was `balance`, and a beneficiary that is another account held its balance less `balance`. The withheld-gas suite's cold host now holds the running account warm, as a journal does, so the read is not counted as a cold load there.
…kes its record back
…ks' facts `SELFDESTRUCT` read its balance before the write inline in `host.rs`, and the creation instruction read the caller in `cache_created_address`. Both now call `instruction_hooks::running_account`, so `host.rs`, an upstream file, gets a call site rather than the read itself. The doc of `cache_created_address` no longer garbles the depth limit.
…hash as computed, or nothing
… keep, a sealed-spec pick needs an argument and tests, and the start charge entry matches the engine Rule 2 now says the fork carries a bounded set today and keeps the stricter test for additions; the exclusive-access sentences are removed. A pick without a switch needs a code-based argument that no sealed spec reaches the changed behaviour, plus regression tests; the outcome check is supporting evidence. The start charge Consumer paragraph describes the history-only hook charge and the creator record counted after the creation returns. The branch name and version tuple leave the new text, and the frame-start entry points at Resource pools.
…with a frame's gas
…t only the instruction can decide
…ller, and the hooks introduction no longer claims every entry cannot be done outside the fork The instruction takes only the forwarded share and settlement adds the child's returned reservoir to what the caller kept, so the old sentence contradicted the contract and rule 1. Rule 2 now allows some accounting beside revm's gas, so the introduction describes the Consumer entry as who uses it and why the fork carries it.
… with no-op defaults, and a debug mirror The frame stack keeps the open start of the transaction and of each frame on it, with checked u32 identities. Every step that would break the pairing is refused with a StartLifecycleError (Exhausted, Stale, NotOpen, NoParent, Unsettled). Host gains frame_split, frame_settle and frame_lifecycle_end, each a no-op by default. The context's local context keeps a lifecycle mirror in debug builds only: a stack driven by the hooks that checks (identity, depth) on every event and resets at the transaction's start. Nothing calls the record yet.
…ners A start opens where revm consumes a frame's init: a NewFrame in EvmTr::frame_run and InspectorEvmTr::inspect_frame_run, the transaction's frame in Handler::execution and InspectorHandler::inspect_execution, and the stand-in in Handler::runtime_oog_result. It settles in Evm::frame_return_result after return_result, and in last_frame_result after settle_first_frame, where revm's halt rule follows the hook. execution_result finishes the lifecycle and catch_error discards it. A refused step is a fatal error of the transaction in every build. The lifecycle module holds the helpers a chain's handler, a custom EVM or a hand-driven runner calls, and states the reuse contract for a caller of run_without_catch_error. The hand-run first frame in reservoir_share and the custom EVM in custom_frame open and settle as revm's runners now do.
… a rebuilt outcome on the settlement revm marks a start's result just before an inspector's end callbacks and checks the marks just after: a StartMark (u32) on the gas tracker and an OutcomeMark (u16) on the call or creation outcome, each in existing padding. The settlement reports inspector_answer, gas_replaced and outcome_rebuilt from what the inspected run recorded on the start. The marks take no part in the values: they always compare equal, hash to nothing, are left out of the debug output, which keeps the derived output, and are not encoded. The outcomes gain the public start_mark field, which a struct literal names.
…it took FrameStart gains reservoir: Option<u64>, None when the hook is called. None keeps the instruction's forward, the schedule's share of the caller's reservoir. Some is the reservoir the frame's input carries: the instruction takes no share of its own and the caller keeps what the hook left (GasTracker::forward_to_child_with_reservoir). The pools are forwarded by the schedule either way. No hook in this workspace sets it.
…o starts share an outcome mark The marks themselves print their type's name; the tracker and the outcomes leave them out of their debug output. Two starts share an outcome mark when their identities are a multiple of 65,535 apart, and the tracker's mark fits padding without naming a field order. The record's sequence field is named for what it holds, the last identity allocated; settle_status is private, and a doc link drops its redundant target.
…ear in every build A transaction whose runtime gas phase runs out of gas is included as a halt on a reused EVM, which needs the stand-in's start to open and settle; the event test keeps the exact events for debug builds.
…settlement hook credited close_transaction_start consumed every unit of regular gas left after a halt, after the hook. revm's own settlement can leave some on a halt: an answer to the transaction's frame whose tracker hands back a reservoir unwinds the transaction's spill into regular gas, and the settlement keeps it. With the default hook the extra spend moved that figure (999,999 to 1,000,000 gas spent on the review's case). The rule now consumes only what the hook credited above what the settlement left, so a hook that credits nothing changes no figure. default_hook_figures runs 136 answer and rewrite shapes, on the transaction's frame and on a child, calls and creations, inspected and answered in a consumer's frame_init, against the figures recorded on b370b53; the review's case is its own test.
…its NewFrame action The open used to follow process_next_action: frame_run bound its result, matched it by reference and rewrapped it, which moved the 264-byte init or result out of the Result and back into the return value. The child's start now opens before the action becomes the init, from the action's kind and the frame's depth (the init's depth is one below the frame's, always), and process_next_action(..).inspect(..) is the return value again, as it was before the lifecycle.
… outlive the transaction The slots were resized one level at a time, and emptied with clear() at every boundary, so every transaction grew them again level by level and a fresh EVM reallocated per new depth. They now grow, cold, to the frame stack's capacity the first time a child opens past them, and are never shrunk; a count of open children answers the boundary's check, and only a discard empties slots. Whether a lifecycle is under way is the last identity allocated being non-zero, which keeps the record at 40 bytes.
…t helper says what it credits
…he frames it needs The record grew its child slots to the frame vector's capacity, which a vector of zero-sized frames reports as usize::MAX: the first child start on such a stack panicked with a capacity overflow. Zero-sized frames now name no room, so their slots grow to the index a child needs; ordinary frames keep growing the slots with the stack's capacity.
…he hook's net regular-gas credit The hook's doc still described the halt rule as consuming the regular gas left on a halt. revm consumes the regular gas the hook credited above what its own settlement left, keeps that gas, and leaves a debit.
…ettles a start, what a handler or runner that replaces those points must call, what the record refuses and what a consumer that sets nothing pays The pairing of a start with its settlement is revm's, so a custom EVM, a handler override and a hand-driven first frame take part in it, and a miss is a fatal error in release too. The entry names those obligations and the sizes; the rule 2 set does not grow.
…rries no start, the settlement types are sealed, every refusal is fatal in every build, the mirror is a LifecycleMirror, the child's settlement goes through Returning and close_frame_start, FrameStart::reservoir is documented, and each of the three API breaks says what a consumer does The entry was written against the prototype; the delivered code differs in these places, and a statement that names a removed item or describes the old behaviour misleads a consumer.
… a halt consumes only the hook's net regular-gas credit, that marks collide a multiple of 65,535 apart, and how the child slots are sized and grown
…sumer init, that the child's return closes the child's start, names the plain and inspected init entry points, states the consumer side as a contract, says boundaries never resize the slots, and states the API exceptions as rules; the generic-hooks row drops its stale size
1.97.0 carries the fix for a closure-call miscompilation. Its clippy adds manual_filter, which one upstream line in bundle_account.rs trips; the line takes the lint's own rewrite.
…e gas tracker and the frame values A consumer keeps a frame start's budgets, the charge its failure gives back and its history allowance on a ledger of its own, driven by the start lifecycle's hooks, so revm no longer carries them: - GasTracker loses its three pools, their nets and crossings and the history allowance (144 to 88 bytes), and Gas the methods forwarding them; PoolId, PoolAmounts and StartCharge go; - CallInputs loses charged_start, pool_shares and history_allowance, CreateInputs its charged_start and pool shares, CallOutcome and CreateOutcome their charged_start; FrameStart loses charge; - return_result and settle_first_frame no longer refund a start charge, a precompile's answer adopts no pool shares, and the schedule loses the pool kept-percent entries (ids 246 to 248) and call_history_allowance (id 253), which stay unused; - ContextTr::admit_code_deposit is no longer lent the creating frame's tracker, which existed for drawing a pool: the signature is the hook's original one. The tests of the removed behaviour go with it.
A frame start copies its caller's EIP-8037 reservoir into the child's input and the caller keeps it while the child runs; the child's return replaces the caller's reservoir with what the child left, once that has unwound the caller's spill. The transaction's frame and the runtime out-of-gas stand-in are handed a copy of the transaction's reservoir the same way. That is upstream's forward and settlement: handle_reservoir_remaining_gas and GasTracker::absorb_returned_reservoir mean what they mean upstream again. The schedule's reservoir_kept_percent (id 249, now unused) and GasParams::reservoir_forward_share go, with GasTracker::forward_reservoir, forward_to_child, forward_to_child_with_reservoir, forward_all_to_child and ChildShares, and settle_first_frame's assertion that the transaction's gas holds no reservoir. A consumer that keeps part of a caller's reservoir takes it in the frame-start hook, names the child's reservoir on FrameStart::reservoir, and adds FrameSettle::held_reservoir back when revm settles the child. The tests that forwarded 98% through the schedule now keep 2% through those hooks and get the same figures.
… charge and the history allowance Rule 1 no longer carries the additive reservoir settlement as an exception, rule 2's set is withheld gas and the history dimension, and the entries for the four figures go. The frame-start hook and the start lifecycle say how revm forwards and settles the reservoir now and how a consumer that keeps part of it adds it back; the code-deposit admission entry is the hook's original one; the sizes are the frame values' without the figures.
…rvoir the frame's input carries
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds instruction hooks, frame-loop hooks and a start lifecycle to the fork, so that mega-evm (MegaETH's execution engine, which runs on this fork) can meter writes and frame starts inside revm's own instructions, keep per-start accounting that follows revm's own pairing of a frame's start with its settlement, and set a frame's next action or rewrite how it ends through revm's own loop, instead of wrapping instructions and loops and reconstructing their state from outside.
The instruction hooks are two
Hostmethods with default bodies:write_completed, whichSSTORE,LOG0–LOG4andSELFDESTRUCTcall after their last charge, andframe_starting, which the call and creation instructions call after the gas forwarded to the new frame is taken and before the frame's input is built.The frame-loop hooks are a check and a method: a frame's run returns an action set on its interpreter before the run (the pending action) before any instruction runs, and
EvmTr::frame_run_resultreceives each frame's run result once, before the inspector's end callbacks and the caller see it.The start lifecycle is revm's record of every frame start and its settlement, reported to a consumer through three
Hosthooks that default to doing nothing. The fork carries no per-start figure of its own: what a consumer decides per frame start, such as a budget, a charge the frame's failure gives back, an allowance or a part of the caller's reservoir it keeps, it keeps beside revm's gas, driven by these hooks. The reservoir is forwarded and settled as upstream does it.Every hook defaults to upstream's behaviour: a consumer that sets nothing gets upstream's execution results. An inspector can see one difference, listed under API exceptions.
Changes
Host::write_completed(&mut self, write: CompletedWrite<'_>, gas: &mut GasTracker) -> WriteVerdict, whose default continues.SSTORE,LOG0–LOG4andSELFDESTRUCTcall it once the write is in the journal and every charge the instruction makes, its refund included, has been made, on the plain and the inspected path. The hook gets the running frame's tracker and may debit and credit it; nothing is charged after it.WriteVerdict::OutOfGasfails the instruction as a failed charge does, andWriteVerdict::Revert(output)ends the frame as a revert; aSELFDESTRUCTso reverted does not self-destruct.Host::frame_starting(&mut self, start: &mut FrameStart, gas: &mut GasTracker) -> bool, whose default returnstrue.CALL,CALLCODE,DELEGATECALL,STATICCALL,CREATEandCREATE2call it after every charge they make and after the forward (the regular gas taken from the caller for the new frame) is taken, before they build the frame's input;falsefails the instruction out of gas and starts no frame.FrameStart(#[non_exhaustive], built withFrameStart::new) carries the start'skind,caller,target,valueandforward, andreservoir, the child's reservoir if the hook took it out of the caller's tracker itself. After the hook the frame's input copies the caller's reservoir as the hook left it, as upstream's instructions do, or carriesreservoirwhen the hook named one, and the caller keeps what the hook left it. The typesCompletedWrite,WriteVerdict,FrameStartandFrameStartKindare inrevm_context_interface::host.CompletedWriteandFrameStartKindare exhaustive on purpose: a kind added later is a compile error in a consumer that matches them, rather than a write or a start that passes a wildcard arm unmetered.FrameStart::targetfor creations, andCompletedWrite::SelfDestruct::balance. ForCREATEandCREATE2,targetis the address the creation takes. It isNoneonly for a creation the handler fails before it takes an address: an endowment above the caller's balance, a caller's nonce at its maximum, or a frame at the depth limit. The instruction computes the address, and aCREATE2's init-code hash, once into the creation's input, and the handler builds the frame from that cache.SelfDestruct'sbalanceis the destructed account's balance before the write, read from the running frame's own account. Since a destruction changes only balances, it rebuilds both accounts as they were before the write from the journal at the hook.revm_handler::execution::settle_first_frameis all of mainnet's settlement of the transaction's frame except the final copy of the gas into the frame's result: the spend, the frame's settlement and the EIP-2780 state-gas refund, each with its halt rule. Mainnet'slast_frame_resultcalls it. A chain whose handler overrideslast_frame_resultcalls it too and applies its own rules before the copy; feat: settle the first frame through the revm fork's shared settlement and call its start lifecycle op-revm#8 does this forOpHandler. Upstream's own steps of the settlement now live incrates/handler/src/execution/first_frame.rs, so a later pick of an upstream change tolast_frame_resultlands there and records the move as its conflict resolution.Interpreter::run_plain(andrun_plain_as_output) andrevm_inspector::inspect_instructionsreturn an action set on the frame's interpreter before the run (upstream'sLoopControl::set_action) before any instruction runs, and on the inspected path before anyInspector::step.InspectorEvmTr::inspect_frame_initdoes not hand a frame whose actionframe_initalready set toInspector::initialize_interp. No API item is added.EvmTr::frame_run_result(&mut self, result: &mut <Self::Frame as FrameTr>::FrameResult) -> Result<(), ContextDbError<Self::Context>>, whose default does nothing.Handler::run_exec_loopcalls it whenframe_runreturns a result, andInspectorEvmTr::inspect_frame_runcalls it before the inspector'scall_endorcreate_endandframe_end, and for a frame without anEthFrametoo; both call it beforeframe_return_result. What it leaves inresultis what the inspector, the caller andlast_frame_resultsee. A result given without a run (fromframe_init) does not reach it. The inspected run still marks a frame with anEthFramefinished after the inspector's end callbacks, as upstream's does.revm_state::Account::original_info_ref() -> &AccountInforeturns the infooriginal_info()returns without cloning it: the stored original, or a default info built once and shared for an account whose original was never set. It is additive; a consumer that compares an account with its original info, as mega-evm's write records do, no longer moves the code's reference count up and down for each comparison.FrameStack: per-transaction ids from a checked counter, a root slot and one slot per frame. A start opens where revm consumes aNewFrameaction (frame_run,inspect_frame_run), at the transaction's first frame and at the runtime out-of-gas stand-in; it closes inframe_return_resultandlast_frame_result; the transaction's boundary finishes the record on success and discards it on a fatal error.revm_handler::lifecycleholds the helpers a handler that overrides those points calls (open_frame_start,open_transaction_start,close_frame_start,close_transaction_start,finish_transaction,discard_transaction).Host::frame_split,Host::frame_settleandHost::frame_lifecycle_endreport it to a consumer and default to doing nothing;FrameSplitandFrameSettleare#[non_exhaustive]and only the record builds them.FrameSettle::held_reservoiris the reservoir the caller's gas held just before revm settled the result into it: revm's settlement replaces the caller's reservoir with the one the frame returns, and a consumer that kept part of the caller's reservoir at the start adds it back there. A record misuse (a missing parent, a stale or unopened slot, an exhausted counter, an unsettled start at the end) is a fatal error of the transaction in every build. Two marks, au32onGasTrackerand au16onCallOutcome/CreateOutcome, sit in existing padding and take no part in equality, hashing,Debugor serde; they let the settlement report an inspector's answer, a replaced tracker and a rebuilt outcome. A debug build checks the pairing withLifecycleMirror; a release build holds no mirror.rust-toolchain.tomlmoves from Rust 1.95.0 to 1.97.0, the first stable release with the fix for a closure-call code-generation bug (release-mode heap corruption with non-genericFnOnce(Vec<usize>)rust-lang/rust#155241, fixed by codegen: Copy to an alloca when the argument is neither by-val nor by-move for indirect pointer. rust-lang/rust#155343); its comment says a later stable release may be taken for a compiler fix. One line of upstream code,BundleAccount::update_and_create_revert'sand_thentofilter(crates/database/src/states/bundle_account.rs), changes with it, for the lint the newer clippy raises; it gives the same result.MEGAETH-FORK.mdgains entries for the two instruction hooks, the start lifecycle, the pending action, the frame-run result hook and the original-info accessor, and updates the failed-charge record entry. Its Hooks preamble says whatserde(default)gives each kind of encoding format, and that every schedule entry the fork adds is zero by default, which is upstream's behaviour. Its "Baseline behaviour the fork changes" section gains theinitialize_interpchange below.REVIEW.md'smega:hookrow carries the format caveat. The "Start lifecycle" entry says where revm opens and settles a start, what a handler or runner that replaces those points calls, what the record refuses and what a consumer that sets nothing pays.MEGAETH-FORK.mdalso states what the fork carries and how that may change:pre_execution::apply_auth_list_eip2780and the one first-frame creation charge that differs from upstream's when the nonce check is off.Defaults equal upstream
initialize_interp.GasTrackerencoding is the base's: the same fields in the same order, with the failed-charge record encoded as an optional number as before, and itsDebugoutput is unchanged. The marks are neither encoded nor printed.revmein release mode: the main develop state tests, the devnet state tests, and the main develop blockchain tests (61,606 passed, 0 failed, 76 skipped). The legacy Cancun and ConstantinopleGeneralStateTestswere not run for this head; CI'srun-tests.shruns them.history_gas_default_path.rs, unchanged by this PR;test_hooks_that_only_observe_change_nothing, which runs a transaction through every hook site with hooks that only observe and pins itsResultGas;test_default_hooks_change_no_figure_the_settlement_computes, for the 136 cases above;test_every_start_copies_the_whole_reservoir_by_default, for the reservoir;test_an_evm_without_the_loop_hooks_gives_the_recorded_callbacks_and_results, which checks every inspector callback, what it is handed and the transaction's result, on both paths, against a record made on the loops before the loop hooks.API exceptions
cargo semver-checks check-release --release-type minoragainst this PR's base (#75's head), for the twelve crates ofscripts/mega/crates.txt:revm-interpreter,constructible_struct_adds_field:CallOutcome.start_markandCreateOutcome.start_mark. A literal that does not name them stops compiling; name them asDefault::default(), or use the constructors.revm-context,constructible_struct_adds_private_field:LocalContext.lifecycle_mirror, in debug builds only. Build aLocalContextwithLocalContext::default()ornew().Hostmethods andEvmTr::frame_run_result(which have default bodies),settle_first_frame, the lifecycle types and helpers,FrameStart::reservoir,FrameSettle::held_reservoir,original_info_refand the new tracker methods are additions.Items the tool cannot see:
initialize_interpis returned without astep. Upstream's inspected run callssteponce first, at the frame's first instruction, and returns the action as thatstepleft it. An inspector whosesteponly observes sees onestepfewer and gets the same result; one whosestepedits the pending action, or changes the context or the state, gets a different result. A frame whose actionframe_initset is not handed toinitialize_interpat all; revm itself sets none.Hostload.SELFDESTRUCT, andCREATEandCREATE2with EIP-8037 off, callHost::load_account_info_skip_cold_loadonce more, for the running frame's own account. The journal holds that account warm, so the read loads nothing cold and charges nothing, but aHostthat counts or refuses loads sees one more.CreateInputsequality. Its derivedPartialEqcompares the cached address and init-code hash. With EIP-8037 off, the input of a creation the instruction starts now holds them, as it already did with EIP-8037 on. It therefore compares unequal to an otherwise equal input that has computed nothing, and to its own serde round trip, which skips the cache.Handler::run_exec_loopor ofInspectorEvmTr::inspect_frame_runcallsframe_run_resultat the same point itself, or the hook is skipped; aninspect_frame_runthat delegates to an inner EVM's reaches the inner EVM's method. This matters only to a consumer that implements the hook.lifecycle::open_transaction_start) and closes it, or the record refuses its child starts (NoParent) and its end (NotOpen). A caller ofrun_without_catch_errorthat drops an error clears the frame stack and discards the lifecycle before reusing the EVM.Handler::last_frame_result,execution_resultorcatch_error: it callsclose_transaction_start,finish_transactionordiscard_transactionwhere the mainnet method does. feat: settle the first frame through the revm fork's shared settlement and call its start lifecycle op-revm#8 does this forOpHandler. An override oflast_frame_resultthat copies mainnet's body settles the same figures, and needs the call to close the transaction's start.GasTrackeris 88 bytes,Gas104,InterpreterResult144,CallOutcomeandFrameResult208,CreateOutcome192,CallInputs208 andCreateInputs216, as at the base. The start lifecycle adds 40 bytes toFrameStack(40 → 80);LocalContextstays 32 bytes in a release build.Hostthat wraps another and forwards each method by hand must forwardwrite_completed,frame_starting,frame_split,frame_settleandframe_lifecycle_end, or it gets their defaults.The consumers checked against these items are under Consumer impact.
Consumer impact
What a consumer does:
CallOutcomeorCreateOutcomeby literal: it namesstart_mark, asDefault::default(), or uses the constructors.Handler::last_frame_result: it callsexecution::settle_first_frameinstead of copying mainnet's body, then applies its own rules and copies the gas, and closes the transaction's start after the settlement.execution_resultorcatch_error: it finishes or discards the lifecycle where the mainnet method does.LocalContextby literal in a debug build: it usesLocalContext::default()ornew().initialize_interp: it gets that action back from the frame's run without astep.run_exec_looporinspect_frame_runand implementsframe_run_result: it calls the hook where the provided methods do.CompletedWriteorFrameStartKind: both are exhaustive, so the match names every kind, and a match without..names every field.frame_starting: it addsFrameSettle::held_reservoirback inframe_settle, since revm's settlement replaces the caller's reservoir, as upstream's does.Consumers checked, by source search for each shape above:
ContextTrorHostimplementation, no override oflast_frame_resultor of the loops, and no frame built from a reservoir (the release on this fork's revm line and two later checkouts).CallOutcome::newandCreateOutcome::new. Its tracers callCreateInputs::created_address(nonce)in theircreatehooks and now read the cached address, which is the same address. Itsinitialize_interphooks only forward to inner inspectors.Hostimplementation; it follows op-revm's handler, and its inspector'sinitialize_interponly forwards.OpHandleroverrideslast_frame_result,execution_resultandcatch_error. feat: settle the first frame through the revm fork's shared settlement and call its start lifecycle op-revm#8 makes the first call the shared settlement and adds the three lifecycle calls; itsOpEvmdelegatesframe_runto revm'sEvmand keeps the providedinspect_frame_run, so the loops call its ownframe_run_result.CallOutcomeandCreateOutcomeliterals already fail against this PR's base, whose outcomes carry the EIP-8037 fieldcharged_state_gas_address.Cost
For a consumer that sets nothing. The figures are callgrind instruction counts (Ir) for the fork's release profile on aarch64, not wall time. Each per-unit figure is (Ir of a long run − Ir of a short run) / the difference in units, so start-up cancels.
memcpycalls;handler/tests/frame_result_size.rspins it at 208 bytes.CREATE149 instructions (+0.95%) and aCREATE2147 (+0.73%): a warm read of the caller and three checks. The destruction balance costs aSELFDESTRUCT119 (+1.5% of a transaction that self-destructs a contract): a warm read of the running account. A creation the handler builds or fails computes no extra hash. These two were measured on an earlier head of this branch and not measured again at this one.CALL(+0.23% on a 64-call transaction), measured on an earlier head of this branch; nothing for a transfer to an account without code or a precompile's answer. The frame-run result hook's default compiles to nothing on the plain path.settle_first_frameis inlined, solast_frame_resultcompiles to the code it had with the steps written out in it.CALLand on the writes, the creation,SELFDESTRUCT,SSTORE,LOGand precompile workloads as a whole, the inspected path, and a creation with EIP-8037 on. The consumer-side effect on a full engine is measured in refactor(satin): keep no copies of revm's figures, account for frame starts in the engine, and start clean after an error mega-evm#409.Test plan
revm-statecompare every field oforiginal_info_ref()(balance, nonce, code hash, code and account id) withoriginal_info()for an account whose original was never set, a stored original, a created, a destroyed and a created-and-destroyed account, an original edited throughoriginal_info_mut, and one replaced byset_current_info_as_original. A shared default with a different account id, and an accessor that lends the current info of a created account, each fail them.transactandinspect_txon both paths, reach every call site and pin what the hook sees and what the instruction does with its answer. 25 of the hooks' 27 tests fail when the instructions stop calling the hooks; the other two check that a write or a start that fails before the hook never reaches it. The tests matchCompletedWriteexhaustively from outside its crate, so#[non_exhaustive]on it fails to compile.held_reservoirback reproduces the figures of an additive settlement exactly;run_plainandrun_plain_as_outputreturn a pending action before any instruction. Through the inspector on both paths: an action set at a frame's start or on its resume is its result; a child started at a frame's start runs before the frame; an inspector cannot halt a frame whose action is set at its start; an inspector's halt or action ininitialize_interpis returned without astep; a frame's run result reaches the hook once, before the inspector, and its rewrite is what the inspector, the caller and the transaction's result see; a result given without a run does not reach it; a frame type that hides once finished still gets its end callbacks.targetis tied to the addresscreate_endreports and to the address formula. The creation's input is compared with a copy whose caches are cleared, which covers theCREATE2hash and the three cases without an address. 306 destruction cases (spec, beneficiary, creation and prior state) check the rebuilt balances against a snapshot taken insideHost::selfdestructbefore the write. Fillingbalancewith zero, or reading it after the write, fails four tests.-Dwarnings, all targets and all features;-D warnings;no_stdon riscv32imac and riscv64imac, for the workspace and forrevm-database;Cargo.tomland noCargo.lock; its only non-source change outside the documents isrust-toolchain.toml.Stacking
This PR stacks on #75: its base is #75's branch, so its diff is this PR's alone. Merge #75 first. Then retarget this PR to
mainand update its branch frommainwith a merge commit; the PR is squash-merged, so the merge commit does not reachmain's history.Labels
mega:hook, for hooks and data fields added in the thin layer, andapi:exception, for the items listed above, as on #75.No release tag is cut for this change yet: while it is under review, consumers pin this branch's head by
rev, and a tag follows once the line is final.