Skip to content

feat: instruction, frame-loop and start-lifecycle hooks, and a borrowed original-info accessor - #76

Open
RealiCZ wants to merge 106 commits into
cz/feat/mega6-s2lite-move-outfrom
cz/feat/mega7-hooks-and-pools
Open

RealiCZ wants to merge 106 commits into
cz/feat/mega6-s2lite-move-outfrom
cz/feat/mega7-hooks-and-pools

Conversation

@RealiCZ

@RealiCZ RealiCZ commented Oct 22, 2026 •

Copy link
Copy Markdown

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 Host methods with default bodies: write_completed, which SSTORE, LOG0–LOG4 and SELFDESTRUCT call after their last charge, and frame_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_result receives 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 Host hooks 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

  • Completed-write hook. Host::write_completed(&mut self, write: CompletedWrite<'_>, gas: &mut GasTracker) -> WriteVerdict, whose default continues. SSTORE, LOG0–LOG4 and SELFDESTRUCT call 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::OutOfGas fails the instruction as a failed charge does, and WriteVerdict::Revert(output) ends the frame as a revert; a SELFDESTRUCT so reverted does not self-destruct.
  • Frame-start hook. Host::frame_starting(&mut self, start: &mut FrameStart, gas: &mut GasTracker) -> bool, whose default returns true. CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE and CREATE2 call 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; false fails the instruction out of gas and starts no frame. FrameStart (#[non_exhaustive], built with FrameStart::new) carries the start's kind, caller, target, value and forward, and reservoir, 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 carries reservoir when the hook named one, and the caller keeps what the hook left it. The types CompletedWrite, WriteVerdict, FrameStart and FrameStartKind are in revm_context_interface::host. CompletedWrite and FrameStartKind are 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::target for creations, and CompletedWrite::SelfDestruct::balance. For CREATE and CREATE2, target is the address the creation takes. It is None only 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 a CREATE2's init-code hash, once into the creation's input, and the handler builds the frame from that cache. SelfDestruct's balance is 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.
  • The shared first-frame settlement. revm_handler::execution::settle_first_frame is 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's last_frame_result calls it. A chain whose handler overrides last_frame_result calls 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 for OpHandler. Upstream's own steps of the settlement now live in crates/handler/src/execution/first_frame.rs, so a later pick of an upstream change to last_frame_result lands there and records the move as its conflict resolution.
  • The pending action. Interpreter::run_plain (and run_plain_as_output) and revm_inspector::inspect_instructions return an action set on the frame's interpreter before the run (upstream's LoopControl::set_action) before any instruction runs, and on the inspected path before any Inspector::step. InspectorEvmTr::inspect_frame_init does not hand a frame whose action frame_init already set to Inspector::initialize_interp. No API item is added.
  • The frame-run result hook. EvmTr::frame_run_result(&mut self, result: &mut <Self::Frame as FrameTr>::FrameResult) -> Result<(), ContextDbError<Self::Context>>, whose default does nothing. Handler::run_exec_loop calls it when frame_run returns a result, and InspectorEvmTr::inspect_frame_run calls it before the inspector's call_end or create_end and frame_end, and for a frame without an EthFrame too; both call it before frame_return_result. What it leaves in result is what the inspector, the caller and last_frame_result see. A result given without a run (from frame_init) does not reach it. The inspected run still marks a frame with an EthFrame finished after the inspector's end callbacks, as upstream's does.
  • Borrowing an account's original info. revm_state::Account::original_info_ref() -> &AccountInfo returns the info original_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.
  • The start lifecycle. revm records every frame start and its settlement in FrameStack: per-transaction ids from a checked counter, a root slot and one slot per frame. A start opens where revm consumes a NewFrame action (frame_run, inspect_frame_run), at the transaction's first frame and at the runtime out-of-gas stand-in; it closes in frame_return_result and last_frame_result; the transaction's boundary finishes the record on success and discards it on a fatal error. revm_handler::lifecycle holds 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_settle and Host::frame_lifecycle_end report it to a consumer and default to doing nothing; FrameSplit and FrameSettle are #[non_exhaustive] and only the record builds them. FrameSettle::held_reservoir is 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, a u32 on GasTracker and a u16 on CallOutcome/CreateOutcome, sit in existing padding and take no part in equality, hashing, Debug or serde; they let the settlement report an inspector's answer, a replaced tracker and a rebuilt outcome. A debug build checks the pairing with LifecycleMirror; a release build holds no mirror.
  • Toolchain. rust-toolchain.toml moves 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-generic FnOnce(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's and_then to filter (crates/database/src/states/bundle_account.rs), changes with it, for the lint the newer clippy raises; it gives the same result.
  • Documents. MEGAETH-FORK.md gains 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 what serde(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 the initialize_interp change below. REVIEW.md's mega:hook row 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.md also states what the fork carries and how that may change:
    • rule 2 names the MegaETH accounting the fork carries inside revm's charges and settlement (withheld regular gas and the history-gas dimension) and the test for adding to it, and says the per-start figures a consumer decides stay beside revm's gas, driven by the lifecycle;
    • a new section groups what the fork carries by class (the upstream gas-core picks, other upstream picks, generic hooks, switches kept for one consumer, and MegaETH accounting) as the plan for the next base-line move;
    • "Tracking upstream" adds that once a consumer spec that runs on this fork is sealed, a pick that changes an execution result lands behind a switch the consumer turns on per spec, or is not taken; taking it unswitched needs a code argument that the changed behaviour is unreachable under the sealed spec, and targeted tests;
    • the state-gas price entry records the changed signature of pre_execution::apply_auth_list_eip2780 and the one first-frame creation charge that differs from upstream's when the nonce check is off.

Defaults equal upstream

  • With nothing set, the default hooks continue, admit and leave a frame's run result alone; a frame's input copies the caller's reservoir and the child's settlement replaces it, as upstream's does; the lifecycle's hooks do nothing; and revm sets no action before a run. Every execution result is upstream's.
  • With the lifecycle hooks at their defaults, every figure is the one revm computed before the lifecycle existed: a halted transaction consumes only the regular gas a settlement hook credits, and keeps what revm's own settlement left. 136 inspector-answer cases (constructor-built gas, replaced trackers, rebuilt outcomes, rewritten statuses, on the plain and inspected paths, for the transaction's frame and a child, call and creation) are compared with figures recorded before the lifecycle existed.
  • What an inspector sees differs in one place, under API exceptions: an action an inspector sets in initialize_interp.
  • The GasTracker encoding is the base's: the same fields in the same order, with the failed-charge record encoded as an optional number as before, and its Debug output is unchanged. The marks are neither encoded nor printed.
  • The Ethereum execution-spec tests (EEST) pass, run by revme in 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 Constantinople GeneralStateTests were not run for this head; CI's run-tests.sh runs them.
  • The workspace suites, upstream's tests included, pass in the three feature sets. Tests that pin the default path:
    • 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 its ResultGas;
    • 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 minor against this PR's base (#75's head), for the twelve crates of scripts/mega/crates.txt:

  • revm-interpreter, constructible_struct_adds_field: CallOutcome.start_mark and CreateOutcome.start_mark. A literal that does not name them stops compiling; name them as Default::default(), or use the constructors.
  • revm-context, constructible_struct_adds_private_field: LocalContext.lifecycle_mirror, in debug builds only. Build a LocalContext with LocalContext::default() or new().
  • The other ten crates: no semver update required. The hook types, the new Host methods and EvmTr::frame_run_result (which have default bodies), settle_first_frame, the lifecycle types and helpers, FrameStart::reservoir, FrameSettle::held_reservoir, original_info_ref and the new tracker methods are additions.

Items the tool cannot see:

  • An action an inspector sets in initialize_interp is returned without a step. Upstream's inspected run calls step once first, at the frame's first instruction, and returns the action as that step left it. An inspector whose step only observes sees one step fewer and gets the same result; one whose step edits the pending action, or changes the context or the state, gets a different result. A frame whose action frame_init set is not handed to initialize_interp at all; revm itself sets none.
  • One more Host load. SELFDESTRUCT, and CREATE and CREATE2 with EIP-8037 off, call Host::load_account_info_skip_cold_load once more, for the running frame's own account. The journal holds that account warm, so the read loads nothing cold and charges nothing, but a Host that counts or refuses loads sees one more.
  • CreateInputs equality. Its derived PartialEq compares 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.
  • The loops' hook. An override of Handler::run_exec_loop or of InspectorEvmTr::inspect_frame_run calls frame_run_result at the same point itself, or the hook is skipped; an inspect_frame_run that delegates to an inner EVM's reaches the inner EVM's method. This matters only to a consumer that implements the hook.
  • A runner that drives the transaction's frame itself opens the transaction's start (lifecycle::open_transaction_start) and closes it, or the record refuses its child starts (NoParent) and its end (NotOpen). A caller of run_without_catch_error that drops an error clears the frame stack and discards the lifecycle before reusing the EVM.
  • Overrides Handler::last_frame_result, execution_result or catch_error: it calls close_transaction_start, finish_transaction or discard_transaction where 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 for OpHandler. An override of last_frame_result that copies mainnet's body settles the same figures, and needs the call to close the transaction's start.
  • Sizes. No value the hooks touch grows: GasTracker is 88 bytes, Gas 104, InterpreterResult 144, CallOutcome and FrameResult 208, CreateOutcome 192, CallInputs 208 and CreateInputs 216, as at the base. The start lifecycle adds 40 bytes to FrameStack (40 → 80); LocalContext stays 32 bytes in a release build.
  • Hand-written forwarding. A Host that wraps another and forwards each method by hand must forward write_completed, frame_starting, frame_split, frame_settle and frame_lifecycle_end, or it gets their defaults.

The consumers checked against these items are under Consumer impact.

Consumer impact

What a consumer does:

  • Builds CallOutcome or CreateOutcome by literal: it names start_mark, as Default::default(), or uses the constructors.
  • Overrides Handler::last_frame_result: it calls execution::settle_first_frame instead of copying mainnet's body, then applies its own rules and copies the gas, and closes the transaction's start after the settlement.
  • Overrides execution_result or catch_error: it finishes or discards the lifecycle where the mainnet method does.
  • Builds LocalContext by literal in a debug build: it uses LocalContext::default() or new().
  • Sets an action in initialize_interp: it gets that action back from the frame's run without a step.
  • Overrides run_exec_loop or inspect_frame_run and implements frame_run_result: it calls the hook where the provided methods do.
  • Matches CompletedWrite or FrameStartKind: both are exhaustive, so the match names every kind, and a match without .. names every field.
  • Keeps a part of a caller's reservoir in frame_starting: it adds FrameSettle::held_reservoir back in frame_settle, since revm's settlement replaces the caller's reservoir, as upstream's does.

Consumers checked, by source search for each shape above:

  • reth: no literal of the outcome types, no ContextTr or Host implementation, no override of last_frame_result or of the loops, and no frame built from a reservoir (the release on this fork's revm line and two later checkouts).
  • alloy-evm, at the release on this fork's revm line: none either.
  • revm-inspectors, at the releases on this fork's revm line: none either. The JS tracer answers starts with CallOutcome::new and CreateOutcome::new. Its tracers call CreateInputs::created_address(nonce) in their create hooks and now read the cached address, which is the same address. Its initialize_interp hooks only forward to inner inspectors.
  • alloy-op-evm, at the release on this fork's revm line: no literal and no Host implementation; it follows op-revm's handler, and its inspector's initialize_interp only forwards.
  • The op-revm fork: its OpHandler overrides last_frame_result, execution_result and catch_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; its OpEvm delegates frame_run to revm's Evm and keeps the provided inspect_frame_run, so the loops call its own frame_run_result.
  • Foundry (master): it requires a newer revm major than this fork's base line, so it cannot take this fork through a version-satisfying patch. Its CallOutcome and CreateOutcome literals already fail against this PR's base, whose outcomes carry the EIP-8037 field charged_state_gas_address.
  • MegaETH's own engines and node are updated in their own pull requests.

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.

  • Against this PR's base, no frame value is larger (see Sizes under API exceptions), so a frame result stays below the size past which the handler's moves of it become library memcpy calls; handler/tests/frame_result_size.rs pins it at 208 bytes.
  • With EIP-8037 off, the default spec's path, the creation address costs a CREATE 149 instructions (+0.95%) and a CREATE2 147 (+0.73%): a warm read of the caller and three checks. The destruction balance costs a SELFDESTRUCT 119 (+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.
  • The pending-action check costs about three instructions per frame run on the plain path: 6.03 per 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_frame is inlined, so last_frame_result compiles to the code it had with the steps written out in it.
  • Not measured against the base at this head: the start lifecycle, the hooks on CALL and on the writes, the creation, SELFDESTRUCT, SSTORE, LOG and 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

  • The workspace has 128 more tests than the base: 748 / 748 / 751 under default, no default and all features, one skipped in each, against 620 / 620 / 623.
  • The original-info accessor. Two tests in revm-state compare every field of original_info_ref() (balance, nonce, code hash, code and account id) with original_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 through original_info_mut, and one replaced by set_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.
  • The instruction hooks. Tests in the interpreter, and through transact and inspect_tx on 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 match CompletedWrite exhaustively from outside its crate, so #[non_exhaustive] on it fails to compile.
  • The reservoir. Tests on the tracker, the handler and the inspector on both paths pin that every start copies the whole reservoir by default and that the child's settlement replaces the caller's:
    • a child that spends past its reservoir onto its regular gas, or cannot pay and runs out of gas, a restoration in the child, and a refill beside a child's spill;
    • a creation, a collision, a creation transaction, a system call and a runtime out-of-gas, each settled once;
    • answers without a frame, and outcomes an inspector builds;
    • a consumer whose hooks keep part of the caller's reservoir and add held_reservoir back reproduces the figures of an additive settlement exactly;
    • the first frame settled into gas that kept its reservoir counts it once.
  • The pending action and the frame-run result hook. run_plain and run_plain_as_output return 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 in initialize_interp is returned without a step; 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.
  • The creation address and the destruction balance. The hook's target is tied to the address create_end reports and to the address formula. The creation's input is compared with a copy whose caches are cleared, which covers the CREATE2 hash and the three cases without an address. 306 destruction cases (spec, beneficiary, creation and prior state) check the rebuilt balances against a snapshot taken inside Host::selfdestruct before the write. Filling balance with zero, or reading it after the write, fails four tests.
  • The start lifecycle. Lifecycle tests run in debug and release and check every event's id, parent and depth; removing any opening point, settle site or boundary call fails them in both profiles, in this fork and in op-revm. Reuse after a fatal child error, an unsettled end, an exhausted counter and zero-sized frames are covered; the inspector cases include a replaced interpreter, a rewritten input depth, a reused allocation and a simulation on a cloned context.
  • Gate, on the pinned toolchain (1.97.0):
    • fmt;
    • clippy with -Dwarnings, all targets and all features;
    • nextest in the three feature sets;
    • doctests;
    • docs with -D warnings;
    • no_std on riscv32imac and riscv64imac, for the workspace and for revm-database;
    • EEST as above.
  • Manifests. This PR changes no Cargo.toml and no Cargo.lock; its only non-source change outside the documents is rust-toolchain.toml.
  • semver: as above.
  • The op-revm fork passes its suite against this PR's head: 107 tests, and 110 with all features, in feat: settle the first frame through the revm fork's shared settlement and call its start lifecycle op-revm#8, the op-revm fork PR that adopts it.

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 main and update its branch from main with a merge commit; the PR is squash-merged, so the merge commit does not reach main's history.

Labels

mega:hook, for hooks and data fields added in the thin layer, and api: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.

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.
…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.
… 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.
…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.
…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.
@RealiCZ RealiCZ changed the title feat: instruction and frame-loop hooks, and resource pools and a reservoir share on the gas tracker feat: instruction, frame-loop and start-lifecycle hooks, and a borrowed original-info accessor Oct 28, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api:exception Registered exception to the superset rule mega:hook A hook or data field added in the thin layer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant