Repository navigation
Conversation
…d where they are read
…swer instead of staging it
…eutral configuration
…ion record charge
…d in the review guide's map
…er's settlement and the neutral check
…ice when an inspector answers the creation
…he handler unprepared The reported reservoir is forgotten where every execution builds its gas, and the result reports its own unless post-execution already reported it, so a system call run through revm's public handler entry points without the crate's preparation no longer reports the reservoir of the execution before it.
The layer keeps one value, set when it begins a transaction or a system call, that says whether it is the protocol's own work, a deposit or an ordinary transaction; the exemption and prices_history derive from it, so a transaction set on the context and not run yet changes neither. prices_history is const again.
…tted it The layer records the body's bytes when validation finds the gas limit covers their history, and the intrinsic history gas and the settled history bytes read that record, so a transaction validation refuses reports none, as before the history figures were recomputed.
… counts no transfer log The count of a value movement's transfer log follows the journal's own EIP-7708 switch, so a caller or an inspector that turns it off through the context's mutable journal gets no log and no 160 bytes; a SELFDESTRUCT moving value is held to a data-size limit of its body and records alone. Documented in AGENTS.md and the record module.
…n a pre-Amsterdam default
… cites the exemption
… run the unprepared system call above the cap
… unpriced for history until it is prepared Switching to the neutral configuration records, in the class the layer keeps, that an ordinary execution pays no history, and the next preparation decides again; with_cfg alone no longer prices history, so a system call run straight through the handler's public entry points on a context switched to the neutral configuration and back pays none, as before the history policy was derived.
Claude review status
🛠️ Review did not finish Attempted head This round did not publish: MODEL_NO_OUTPUT: Claude returned no structured review output. Anything listed below is from the last round that did. Workflow run: https://github.com/megaeth-labs/mega-evm/actions/runs/40804560927 |
Merging this PR will regress 4 benchmarks2 warnings
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
Codecov Report❌ Patch coverage is ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
🧬 Mutation testing — ✅ PASSDiff mutation score: 100.0% (153/153 viable mutants killed)
No new test gaps introduced by this change. 🎉 |
…nd share one settlement lookup
…ver the docs still put them on the gas tracker
…iew guide and a test's doc
…r gas its settlement left
…aces keeps its history allowance
…de draws the creation's budget, and the engine has no separate inspected lifecycle
… settlement left, and the halt tests skip a free history byte
…wherever the docs still left it to revm's rule
…uts the reservoir
…all's journal transaction
…hat a failed _one call leaves in the journal
…etry a _one call after a failed deposit recovery
…h what the child hands back, and the transaction's gas holds what its frame hands back
…draws No provider the engine runs draws a pool, so the draws read back off revm's answer were none, and adopting none left the start's entry as its split opened it. The read-back and its wiring test go; the start ledger's fail-closed adoption stays, with its unit test, as the rule a provider that draws on its answer is adopted by.
…tart's four figures Pins the revm fork and the op-revm fork at the commits where the pools, the start charge, the history allowance and the reservoir share leave the fork and revm forwards and settles the reservoir as upstream does. The pins name local commits by full SHA until the forks are pushed. - Host::frame_settle gives a child's caller back the reservoir it held while the child ran (FrameSettle::held_reservoir), the part forward_start kept, on top of what revm's settlement put in place of the caller's reservoir. The transaction's gas holds a copy of its frame's reservoir, which the settlement replaced, so nothing is added back at the transaction's start, and a halted transaction's regular gas is still burned after the start charge is given back. - The pool identifier and amounts of a frame's budgets are the engine's (limit/pool.rs), exported beside LimitKind for the start ledger's test recorder. - MegaContext::admit_code_deposit takes the deposit alone, the hook's signature in the fork; the outcome and input literals lose the removed fields, and two hand-settled tests hand the frame a copy of the reservoir rather than taking it. - The schedule test asserts the engine's own constants; the rendered pricing table loses the five entries the fork no longer has; the reference holding of a child's shares runs on explicit figures; the keyless differential record's digests move where the record rendered the tracker's removed fields, and only there.
…gives a child's caller back the reservoir it kept
…, and halts the transaction's frame after a value transfer above and below the cap
…e share and spec-number tests pin
…for a start's shares, and the module table lists the pool types
…ld are pinned, and two comparisons no mutant can tell apart are gone - A unit test pins that only a caller's negative net holds its child's share to the caller's budget: a net back at zero after a take-back and a draw again holds the child to the frame cap alone. - A unit test pins that an answer rebuilt on its caller's forward puts its start back at its shares, undrawn. - `signed` saturates through `i64::try_from`, with no comparison at `i64::MAX`, where `>` and `>=` agreed. - A precompile's draw is held to its share with `min`: a draw at, past or within the share gives what the two branches gave, without the comparisons whose `>`/`>=` forms agreed at zero and at the share.
… an inspector has no budget field left to write
… by an index loop
Summary
This pull request simplifies the Satin engine by deleting the engine's own copies of figures that revm, the journal or the transaction already hold: each one is now computed where it is read, read off the journal, or decided once when the layer prepares an execution. It changes no consensus outcome: the whole-outcome digest of every execution-spec fixture is equal to its base in equivalence mode, Satin mode and Satin under tight limits, and no insta snapshot changes. One tool-facing behaviour changes on purpose (see "Behaviour").
It also moves the accounting of a frame's start into the engine: the three resource budgets a child starts with, the part of the reservoir its caller keeps, the start charge a failed start gives back and the history allowance of a value call are now decided by the engine on a ledger of its own, driven by the revm fork's start-lifecycle hooks, and the forks carry none of them. And it makes an EVM used again after a call that returned an error start as a fresh one would. Neither changes a transaction's outcome; the inspector shapes whose behaviour changes are listed under "Behaviour".
It is the first pull request of the work after #407: simplification first, then the performance work, on this branch. The performance part takes back about a third of the per-transaction instruction count #407 added on realistic workloads, again with every outcome unchanged. It merges into
cz/feat/satin-w2(#386).Changes
MegaContext::intrinsic_history_gas()gives the body's history once validation admitted it, and 0 otherwise.Frame-start accounting in the engine
limit/ledger.rskeeps one entry per open frame start: the start's three budgets with their nets, its start charge and its history allowance. revm's start lifecycle drives it alone (Host::frame_split,frame_settle,frame_lifecycle_end), so the engine holds no pairing logic of its own: the ledger begins at the transaction's start, takes each child's start as revm opens it, settles it as revm settles the frame, and is dropped at the lifecycle's end, settled or discarded.FrameStart::reservoir. The limit checks and every draw read the running start's entry. A start that did not succeed gives back its start charge (a creation also when it took no address), except for an inspector's answer or an outcome an inspector rebuilt; a valueCALL/CALLCODEgives its frame the history allowance, which a log draws first. A precompile's draws on a start's budgets would be adopted fail-closed against the start's share; no provider draws them today.FrameStart::reservoir, and the settlement replaces the caller's reservoir with what the frame returns. The engine gives the caller back the part it kept (see "Running on forks that carry none of the four figures").ed2d8d95and the head on the forks that carry none of the figures: every line is equal, and no inspector shape moved again.Running on forks that carry none of the four figures
Host::frame_settleaddsFrameSettle::held_reservoir, the reservoir the caller held while the child ran, back on top, at depth 1 and below; the caller then holds what it kept and what the child handed back, on every path that settles a child: an instruction's start, a precompile's answer, a start revm refuses, an inspector's answer and akeylessDeploycreation. Nothing is added back at the transaction's start: revm hands the transaction's frame a copy of the transaction's reservoir, which its settlement replaces, so adding it would count it twice. The result equals the additive settlement the forks had on every amount.PoolIdandPoolAmountsleft the fork with the pools; they are inlimit/pool.rswith the API andDebugshape they had, and exported from the crate root besideLimitKind.ed2d8d95left five survivors inlimit/ledger.rs. Two are killed by new unit tests (a caller's negative net holds its child's share to its budget, and an answer's rebuild puts its start back at its shares). Three are written away: two comparisons no mutant can tell apart, and a third, the signed conversion, written without a comparison.tests/satin/reservoir_settlement.rspins, for a child that writes and stops, reverts, halts, spills past its share and unwinds, what its caller holds after the settlement, and the transaction's own frame, which halts above and below the cap with nothing added back. It is red without the add-back and red with the add-back at the transaction's start too.Reuse after an error
MegaHandler::catch_error) now clears a database error a load stored in the context and the handler never took (post-execution failed first, on a read of its own), takes back the failed call's journal transaction (its nonce bump and fee, and the writes of frames that completed before the error), and clears the per-transaction caches and the frame stack. A failed deposit is a result, not an error: its nonce bump and mint stay.MegaEvm::replay,system_call_with_callerand the pre-block calls now do, astransactand the inspected entry points already did. The_oneentry points leave the journal to the caller on both outcomes, without the failed call's writes.Performance
STATICCALL, aDELEGATECALLor a zero-valueCALL/CALLCODEwhose target is not a precompile changes no account at its start, so the before-and-after reads find nothing; they are skipped, and the opcode's start pricing takes the same short cut by a rule of its own. A precompile call, a node's stateful precompile included, and aCALLCODEwith value are still read. A debug build makes the skipped reads and asserts they find nothing.SELFDESTRUCTneeds it.Account::original_info_ref, instead of a clone of the original info for every comparison. Measured on its own before the changes above, it saved about 330 instructions per transfer.Per transaction, on arm64 callgrind with CodSpeed's simulation flags (the CodSpeed report on this pull request is the reference):
The op-revm control arms are unchanged at every commit. The table predates the last item, whose saving on top of the others the CodSpeed report shows.
The start ledger's cost, measured the same way on the forks that carry none of the four figures, against
ed2d8d95(the head with the ledger on forks that still carried them) and against98a1848d(the head before the ledger):ed2d8d9598a1848dCALLdeep_callsSSTORELOG2The op-revm arms of the call benchmarks drop by 529 to 531 per
CALLagainsted2d8d95. Most of it is the size of the frame results: at 208 bytes they are copied inline again, where at 296 every move through the handler's wrappers was a librarymemcpycall, which thememcpyfigure of a value-call benchmark shows (−527 perCALLon the Satin arm). The ledger's own cost per write is what remains against98a1848d(+55 perSSTORE, +20 perLOG2). It is measured, not tuned: the optimisation candidates are listed and are taken together with the rest of the performance work at the end of the line.Tools and benches
state-test --json-witness(and--json-witness-full) prints, per transaction and per block, the set of everything the engine read below the journal: accounts, code, storage, block hashes, SALT buckets and oracle slots. It runs in the same pass as--json-digest, whose output is unchanged. A change that keeps every outcome but reads different state now shows up.block/erc20_transfers: a block of 64 ERC-20 transfers, the token's source and build command beside the bench, with an unmeasured preflight that checks each transfer's event, return value and balances.Toolchain
nightly-2026-04-14tonightly-2026-04-23, the first 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). Onlyrust-toolchain.tomlchanges. Under the two compilers the outcome digest and the read-set comparison are equal (six runs, and 20,271 blocks of blockchain tests). This PR's CodSpeed report from that commit on includes code-generation changes; the instruction counts in the table above were measured before it, on the old compiler.Forks
4c8f0d43) and feat: settle the first frame through the revm fork's shared settlement and call its start lifecycle op-revm#8 (8c0db939). Both forks are checked with Rust 1.97.0.Spec
satin.mdnames Satin's EIP-8037 profile with its freeze point: the base commit, the later commits taken and declined, and Satin's own departures. A later change to EIP-8037 is an input to a later spec, not to Satin.Behaviour
untouched_call_gasoruntouched_create_gas) is settled as an answer: unlimited budgets for the caller and no start-charge refund;None of these is on a block-building, validation or replay path.
Public API
AdditionalLimit::intrinsic_history_gas()is replaced byMegaContext::intrinsic_history_gas(), with the same figure.MegaContext::prices_history()is no longerconst.PoolIdandPoolAmountsare exported from the crate root.HISTORY_GAS_PRICEDno longer lists the call's history allowance entry: the engine grants the allowance itself.untouched_call_gas/untouched_create_gasno longer put budgets on the gas they build.test-utils:record_start_ledger_settlements,take_start_ledger_settlements,LedgerSettlementandLedgerPoolslet a test read what the start ledger settled.Not in this pull request
Verification
The frame-budget boundary of a completed write's count (a count equal to the frame's remaining pool is within it) is pinned by a test, and a redundant guard the comparison already implied is gone; the outcome digest of that change is equal to its base.
The accessor commit, built against the two fork heads: outcome digest and read-set comparison equal to its parent (six runs, and 20,271 blocks of blockchain tests).
Performance commits: outcome digest against
edf3fa1e(equivalence, Satin and tight runs on Osaka and Amsterdam), all equal; the read-set comparison against the commit that added it, all six runs and the blockchain tests (20,271 blocks) equal;cargo test --workspace --release --lockedwith warnings denied: 1,867 passed, 0 failed.Outcome digest against
2ec47583(the base): equivalence, Satin and tight runs on Osaka (19,465 transactions) and Amsterdam (15,754), all equal, no panic.cargo test --workspace --release --lockedwith warnings denied: 1,853 passed, 0 failed. The CI insta run: no snapshot changed. Fuzz seeds 0 to 8,mega-evme,scripts/price_grid.sh --pr, the benches in test mode, and the debug state-test in equivalence and Satin modes with no panic.fmt, clippy (CI flags), cargo sort, the
no_stdcheck and Prettier pass.New tests pin the reservoir of an unprepared direct system call, the prepared history policy and the validated intrinsic history, a context switched back from the neutral configuration, the journal's EIP-7708 switch, and the keyless signer's re-priced give-back in a crowded SALT bucket; each fails without what it pins.
Start ledger, reuse after an error (head
ed2d8d95): outcome digest and read-set comparison against98a1848d(the previous head of this pull request), all six runs and the blockchain tests (20,271 blocks) equal; the same runs with debug assertions compiled in, equal and without a panic; the engine's counterfactual replay of three recorded mainnet blocks byte-identical;cargo test --workspace --release --lockedwith warnings denied: 1,898 passed; the long fuzz run, clippy, fmt,no_stdin debug and release.The mutation gate runs in CI; the survivors it reported at
ed2d8d95are resolved (see "Running on forks that carry none of the four figures").The forks without the four figures. Every gate below ran on a tree identical to
dd007b9aexcept that its fork pins pointed at local clones of the same fork commits:cargo test --workspace: 1,912 passed, 0 failed, 4 ignored; the long fuzz run (MEGA_FUZZ_LONG=1): 23 passed;cargo test --workspace --release --lockedwith warnings denied: 1,901 passed, 0 failed, 4 ignored;no_stdon riscv64 in debug and release with warnings denied, Prettier on the documents, and the benches in test mode (57Success);ed2d8d95and from4f431170to that tree: all six runs equal (19,465 and 15,754 transactions, equivalence and Satin modes, and tight limits), no panic; the read-set comparison: all seven runs equal, 20,271 blocks of blockchain tests;ed2d8d95: lines and summaries equal in all seven runs, no panic, no start-ledger message;ed2d8d95and that tree: 360,000 lines per side, all equal.The legacy execution-spec suites (Cancun, Constantinople) were not run for this head; CI's
run-tests.shruns them.