Skip to content

feat!: meter Satin's writes and frame starts in the revm fork's hooks, on a per-frame byte pool - #407

Merged
RealiCZ merged 487 commits into
cz/feat/satin-w2from
cz/feat/satin-w9
Oct 23, 2026
Merged

RealiCZ merged 487 commits into
cz/feat/satin-w2from
cz/feat/satin-w9

Conversation

@RealiCZ

@RealiCZ RealiCZ commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This pull request moves Satin's resource metering into the MegaETH fork of revm and finishes the engine's resource rules. Its base is cz/feat/satin-w2, the branch of #386, which holds the stacked pull requests #387, #388, #390, #399 and #400 (with #404). This pull request merges into it before #386 merges into satin.

Writes and frame starts are charged inside revm's own instructions, through two hooks. A frame's data-size, KV-update and state-growth budgets are pools on revm's gas tracker, forwarded to a child and settled on its return as the EIP-8037 reservoir is. What a halting frame burned is read off a record the fork keeps. Inspectors run on revm's own inspected lifecycle, and the engine's per-frame work sits only in the frame start, run result and return that revm's plain and inspected loops share. Satin no longer rebuilds revm's state from outside: the instruction wrappers for writes and frame starts, the per-frame record lanes, a journal wrapper with its ledger, the engine's inspected copies of the frame lifecycle and the inspector admission gate for block execution are all deleted.

Several Satin rules change with it (see "Satin rule changes"):

  • write records follow the net change of an account, and a creator's nonce record is counted on the creator when its creation returns;
  • the KV-update and state-growth limits are back, per transaction, per frame and per block;
  • a child is forwarded 98/100 of its caller's regular gas, of its reservoir and of each of its three budgets;
  • an operation that crosses several limits stops on the first of data size, KV updates and state growth, naming the frame budget when that budget is crossed;
  • a frame that a failed callee leaves over the transaction's data-size limit is stopped as it resumes;
  • the first SELFDESTRUCT of an account created in the same transaction gives back what the transaction made of it;
  • gas detention counts the work a callee runs on a value call's stipend, and never counts what a halting frame burns;
  • a frame start is judged on what revm decided;
  • the pre-block calls run on one fixed gas budget;
  • several places where the specification and the engine disagreed now agree.

The state-test runner gains a whole-outcome digest. Every step of this work was held to the step before it with that digest, and each change it showed is explained under "Verification".

  • Metering in the fork. write_completed and frame_starting replace the write and start wrappers. The fork's pools replace the per-frame lanes. The fork's failed-charge record replaces the code that noted halts. The context's journal is revm's own again.
  • Inspectors on revm's lifecycle. The engine's inspected copies of the frame lifecycle and its step guard are removed with src/evm/inspector.rs. A result an inspector rewrites, or an answer it gives, is taken as revm takes it. Block execution admits any inspector.
  • The fork's loop hooks. An action the engine sets on a frame comes back through revm's own frame run, and gas detention classifies a crossing in revm's run-result hook, so inspectors and tracers see the stop the caller gets.
  • Write records follow the net change. A record exists while an account differs from what the transaction loaded, or while a slot differs from its original value. A creation is charged exactly the records it makes.
  • The creator's nonce record at the return. It is counted on the creator once the creation returned with the nonce bumped, and the creation's budget is the share taken at the opcode, before it. The frame-start hook only prices; nothing is counted before revm decides.
  • KV updates and state growth. Both are limited again at Rex6's numbers, per transaction, per frame and per block, as data size is, by a limit stop of the frame that crosses.
  • The destruction table. A same-transaction destruction refills the state gas of what the transaction made of the account, lowers state growth, and takes back its write records and its deployed code's data size and history gas.
  • The 98/100 forwards. A child gets 98/100 of its caller's regular gas, of its reservoir and of each budget.
  • Gas detention. Work run on a value call's 2,300 stipend is compute. What a halting frame burns is not compute, so a detained transaction's compute never exceeds the limit.
  • Frame starts. A start is held to the limits once revm decided it, not by a prediction of what revm will refuse.
  • Pre-block calls. Each runs on PRE_BLOCK_CALL_GAS_LIMIT (31,566,720), whatever the block's gas limit.
  • Spec and engine aligned. A creation's own account loads are not volatile reads. A Satin genesis key given twice is read as its last copy. satinTime is also read from the object form serde_json produces. A witness replay's exports may be a subset of the building node's. The specification states the revert-or-halt rule, a value call's own charges under detention and the EIP-2935 contract's read.
  • Dependencies. The revm fork is pinned by rev at feat: instruction, frame-loop and start-lifecycle hooks, and a borrowed original-info accessor revm#76 and the op-revm fork at feat: settle the first frame through the revm fork's shared settlement and call its start lifecycle op-revm#8, on their public repositories.

Review the diff against the base as a whole: a later commit may revise an earlier one, and the history explains steps, not the final rules.

Terms

  • Satin: the unstable spec of MegaETH's new execution engine, mega-evm 2.x on the MegaETH forks of revm 40.0.3 and op-revm 20.0.0.
  • Frame: the execution context of one call or creation. The transaction's own frame is the first; a frame a call or creation starts is its caller's child.
  • Data size: the bytes a transaction appends to the chain's state and history: its body, write records, logs, deployed code and oracle hints. It is held to a per-transaction limit (13,107,200 bytes) and to a per-frame budget: a child gets 98/100 of what its parent has left.
  • Write record: one per account or storage slot a transaction keeps written, 40 bytes of data size and 40 bytes of history. The KV count is the number of write records, held to 500,000 per transaction.
  • State growth: the net count of new accounts and new storage slots a transaction keeps, held to 1,000 per transaction.
  • History gas: gas for the bytes a transaction appends to the chain's history, at MegaETH's cost per history byte. It is one of three gas ledgers, beside regular gas (compute) and EIP-8037 state gas.
  • Gas detention: once a transaction reads volatile data (the block environment, the block beneficiary's account, the Oracle contract's storage), the regular gas it may still spend is capped at its compute at the read plus 20,000,000. A frame whose regular charge needs gas past the cap is stopped, and its caller resumes.
  • Compute: the regular gas a transaction's frames spend on what they run; the figure detention caps.
  • Limit stop: the revert, with MegaLimitExceeded(uint8 kind, uint64 limit), of the frame that crosses a limit: kind 0 data size, 1 KV updates, 2 compute, 3 state growth. Its caller continues as after any revert.
  • Pool: one of the revm fork's three per-frame budgets on its gas tracker (GasTracker), for data size, KV updates and state growth. Each is forwarded to a child at a frame start and settled on the child's return, as the EIP-8037 state-gas reservoir is.
  • Reservoir share: the part of its caller's EIP-8037 reservoir a child is forwarded: all but 2 per cent, whatever regular gas the call asks for.
  • Start charge: the history gas a frame start charges its caller for the records the started frame makes, declared on the start. The fork gives it back if the frame fails.
  • Failed-charge record: the regular gas a frame had before a charge that failed. The fork keeps it, so the engine knows exactly what a halting frame burned.
  • Record lanes: the engine's own per-frame list of the records each frame made, kept beside revm's journal and merged or discarded as frames ended. This PR deletes them.
  • Inspector: revm's callback interface for tracers, debuggers and Foundry. An inspector can answer a frame in place of running it, or rewrite a frame's result.
  • Destruction table: the rule for the first effective SELFDESTRUCT of an account created in the same transaction: each part the transaction made, absent at its start and present before the destruction, is refilled and taken back, and each part present at its start is an ordinary deletion.
  • Outcome digest: a hash of the canonical record of a transaction's whole outcome: result, state, gas by ledger, usage counts, logs and oracle reads.

Changes

Gas detention and frame starts

  • Detention keeps the frames' compute figures from the transaction's first frame on. The lazy rebuild at the first read, and its debug copy, are deleted.
  • A callee's work on a value call's stipend counts as compute: the stipend is not forwarded gas, so a caller's contribution while its child runs takes off the forward alone. The stipends of the value calls that returned are counted apart.
  • A frame start's records and transfer log are read off the journal before revm decides the start, and counted and held to the limits on the started frame's pools once it has. This replaces the prediction of which starts revm refuses, along with its staging code and its debug cross-check.
    • A frame revm built returns the stop before its first instruction.
    • An answer revm gave without building a frame gives way to the stop on untouched gas, with a success's value move and log taken back to a journal mark.
  • Before gas detention answers a priced precompile without running it, it checks that the caller can fund the call's value. That check now compares only the value the call moves. Before, a DELEGATECALL's apparent value counted too: a DELEGATECALL from an underfunded sender ran a priced precompile past the cap.
  • A value call's own regular charges (its static gas, its memory, the value transfer and a cold access) are decided first, and one that crosses the cap stops the calling frame before anything moves, whether or not the caller can fund the value. The engine already did this; the specification now says so, and a test pins it for a cold and a warm contract, a precompile and an account with no code, funded and unfunded.
  • A call to the EIP-2935 history contract is a read of the block environment, through the NUMBER its code runs before it loads the slot; an SLOAD of the storage the pre-block calls wrote is no read. The engine already did this; the specification now says so, and a test pins both contracts.

The fork's failed-charge record

  • Satin reads what a halting frame burned off the fork's failed-charge record. The halt-noting code and six instruction wrappers that reconstructed it are deleted. An inspector's refused charge leaves its record as an opcode's does, as revm leaves it.
  • The fork's new switch back to revm 40.0.3's journal semantics is fixed off, whatever a caller sets. That holds in Satin's configuration and in the neutral configuration, which runs Ethereum's fixtures at the fixture fork's prices for the execution-spec gate.
  • Two jobs move from the fork into Satin's handler: splitting a system call's gas (SYSTEM_CALL_REGULAR_GAS_LIMIT, 30,000,000 regular, the rest reservoir, in MegaHandler::tx_gas), and reading the reservoir a result settled with (AdditionalLimit::reservoir_remaining). The op-revm baselines run op-revm through a reference handler that does the same.

The fork's instruction hooks

  • SSTORE, LOG0–LOG4 and SELFDESTRUCT call write_completed once the write is in the journal and every charge they make has been made. The engine counts the record, holds it to the limits, and then charges the record's history. A write that the opcode's own failure takes back is never counted.
  • The four calls and the two creations call frame_starting after the forward is taken. The engine charges the caller, out of what it kept, the history of the records the start makes, and declares the frame's own part on the start (FrameStart::charge), which the fork refunds when the frame fails. The hook counts nothing.
  • The write and frame-start wrappers are deleted. Detention holds a frame and reads its compute off the fork's GasTracker.

The fork's loop hooks

  • revm's frame run returns an action set on a frame's interpreter before it runs, without running an instruction or calling the inspector's step, and the inspected frame start hands a frame built with one to no initialize_interp. The keylessDeploy call's creation and answer, and the stop of a caller its child left over a limit, are set that way, so MegaEvm::frame_run only forwards to revm's.
  • revm's plain and inspected loops call the run-result hook (EvmTr::frame_run_result) once a frame's run returns, before the inspector's call_end, create_end and frame_end and before frame_return_result. Gas detention turns a crossing into the stop there, so an inspector and a tracer see the frame end with MegaLimitExceeded(2, limit), as the receipt bills it. A frame that ends before it runs is classified where it ends.
  • An inspector sees a keylessDeploy call start, its creation and the call end, with no initialize_interp or step for the call's own frame. The keyless differential digest and the tracer snapshots follow.

Inspectors on revm's own lifecycle

  • The engine's inspected copies of the frame lifecycle (inspect_frame_init, inspect_frame_run), its step guard (StepGuard) and src/evm/inspector.rs are removed. MegaEvm's InspectorEvmTr is revm's inspected lifecycle unchanged, and it starts, runs and returns frames through the same frame_init, frame_run_result and frame_return_result the plain loop uses.
  • A result an inspector rewrites, and an answer it gives in place of a frame, are taken as revm takes them: the caller settles what the inspector hands back, with no correction of the engine's, and an answer never reaches the engine's frame start, so the depth guard, the interceptors and gas detention see none of it. The rewrite settlement that billed a revived frame as one that kept nothing is removed.
  • A start an inspector rewrites after its opcode is not charged again: what the opcode charged stands, and its declared part comes back only as for any start whose frame keeps nothing (a frame that fails, or a start revm refuses before it bumps a nonce or moves a value). Its records are those revm then made. A creation an inspector answers counts no creator's record, since revm bumped no nonce.
  • Synthetic frame results settle through revm's own frame return. The public settle_frame_result and with_pools_of are removed.
  • Inspectors are not protocol: the specification says nothing about them, and AGENTS.md holds the engine's contract with them.

Outcome-digest tooling

  • state-test --json-digest prints one line per executed transaction, carrying the hash of the canonical record of its whole outcome; --json-digest-full adds the record itself. mega-state-test::digest exposes the canonical record and the hash. The record carries the state growth with the other usage counts.
  • --limit-data-size, --limit-frame-data-size, --limit-kv-updates, --limit-state-growth and --limit-compute-gas run Satin mode under limits tighter than the spec's, so the stop paths run across the fixtures.
  • A base-against-head comparison of those lines over both fixture releases, in both modes and under the tight limits, is the equality check each step below was held to.
  • Its sensitivity was shown on a scratch change that moves 100 gas from the history ledger to the regular one and keeps the total. The digest flagged it on every Satin run; a comparison of the fixtures' verdicts alone (--json-outcome) did not.

The pools, after a journal-centred ledger

  • An intermediate step wrapped revm's journal (MegaJournal) and kept beside it a ledger of what the transaction keeps. The ledger first ran as a shadow, checked against the per-frame record lanes; then it decided, and the lanes were deleted.
  • The final step deletes the journal wrapper and the ledger. What a transaction keeps is drawn on the frames' pools where it is made:
    • a start's records and transfer log on the started frame;
    • a creator's nonce record on the creator, once its creation returned;
    • a completed write, from the write hook;
    • deployed code, at the fork's code-deposit admission.
  • A failed frame gives its draws back with its gas. The transaction's limits are held beside the pools, on what the transaction keeps.
  • One entry per start revm decided restores what the frames keep to what they kept when the start began if its frame keeps nothing, and carries the credit for a record a kept start took back, refilled on the child's result before revm merges it. For a creation it also carries the creator as it stood before the start, which the creator's nonce record is read from at the return.
  • A frame that a failed callee returns into is held to the transaction's limits before it runs on. The failure puts back what the frames above kept at the callee's start, a record of theirs the callee took back included, while an Oracle hint the callee sent into the room that freed stays counted; the caller then returns the transaction's data-size stop, whether the callee reverted, halted or was stopped.
  • A start's records, but a creator's nonce record, are drawn on the started frame's pools, inside its 98/100 share. Their history is charged to the caller.
  • The context's journal is revm's own Journal<DB> again.
  • Contract change for node tooling: the counts follow the frames, not the journal. A journal checkpoint that a node's tool opens and reverts outside the frame lifecycle takes the writes back but not the counts. No transaction can do this; only a tool or an inspector can. Three tool-checkpoint tests went with the old contract.
  • The shares and the history allowance of a value call (160 bytes, one three-topic event carrying a word) are entries of the Satin schedule: pool_kept_percent = 2 for each of the three pools, reservoir_kept_percent = 2, call_stipend_reduction = 50 and call_history_allowance = 14,080 gas. The pricing table lists them.
  • The byte-pool step alone took 489 lines out of mega-evm's source. The ledger (590 lines) and the journal wrapper (343) are deleted.

KV updates and state growth

  • Each transaction is held to TX_KV_UPDATE_LIMIT = 500,000 write records and TX_STATE_GROWTH_LIMIT = 1,000 new accounts and slots, and each frame to 98/100 of what its parent has left in each, on two more pools of the gas tracker (limit::KV_UPDATE_POOL, limit::STATE_GROWTH_POOL). A crossing is a limit stop of kind 1 or 3 (LimitKind::KvUpdate, LimitKind::StateGrowth, Rex6's discriminants).
  • The KV count is the write-record count. State growth rises by one where EIP-8037 charges the state gas of one new account or slot, and for the caller a failed deposit creates, which pays no state gas; it falls when a slot whose original value is zero is written back to zero, and for what the destruction table takes back. A frame's net is signed: a successful child's net merges into its caller, a failed one's is discarded, and a negative net never enlarges a child's share (cap_the_shares). A transaction reports max(net, 0) (LimitUsage::state_growth).
  • An operation that crosses several counted dimensions stops on the first of data size, KV updates and state growth (limit::COUNTED). Inside each, the frame budget is tested before the transaction limit, and the stop names the budget whenever the budget is crossed.
  • Before the first frame, an EIP-7702 list long enough to cross either limit is simulated in order, writing and warming nothing; a shorter list cannot cross and is applied as EIP-7702 applies it. If the KV or state-growth totals it would keep cross the transaction's limit, none of it is applied or charged, and the transaction's own frame is stopped before it runs (hold_authorization_list). A database error in the simulation leaves the transaction without an outcome: an internal error when the database calls it fatal, a block verdict otherwise.
  • The caller account a deposit-like transaction creates counts as one unit of state growth, a failed deposit's included, for the transaction and for the block.
  • The block holds two more packing budgets, BLOCK_KV_UPDATE_LIMIT = 500,000 and BLOCK_STATE_GROWTH_LIMIT = 1,000, checked after the data size and in that order: the transaction that reaches one is included, and every later ordinary transaction is refused before it runs. Deposits and Mega System Transactions are never refused by them and count towards them; the pre-block calls count towards none. BlockLimits may hold either below the protocol's value, never above.
  • The protocol's own work, a system-originated transaction or a system call, is exempt from both limits as from data size; its usage is counted and reported.
  • ProtocolLimits and EvmTxRuntimeLimits read a limits file of the first published shape, such as one given to mega-evme --override.limits, with the default spec's values for the four newer fields. Every field that shape had stays required, and an unknown field is refused.

The 98/100 forwards

  • A call or a creation forwards at most remaining − remaining × 2 / 100 of its caller's regular gas, in place of EIP-150's 63/64 (GasId::call_stipend_reduction = 50). A value call's 2,300 stipend is added after the cap.
  • Every child receives its caller's reservoir less 2 per cent of it (GasId::reservoir_kept_percent = 2), whatever regular gas the call asks for. The transaction's own frame and a system call's outermost frame take the whole reservoir, and a keyless deployment's creation takes the share of what the call has left.
  • The neutral configuration keeps EIP-150's forward and the whole reservoir, so the execution-spec gate's equivalence mode is Ethereum's forward.

The destruction table

  • The first effective SELFDESTRUCT of an account created in the same transaction applies the specification's destruction table to every part of that account.
  • A part absent at the transaction's start and present before the destruction (the account leaf, the deployed code, a slot written non-zero) has its state gas refilled at the price it was charged, to the destroying frame, even when another frame paid it. The leaf and each such slot lower state growth by one, and their write records are taken back with their data size and history gas; the code's data size and history gas come back too.
  • A part present at the start is an ordinary deletion: no refill and no fall. A slot's value at the start is its original in the transaction; a slot of the created account first read after the creation reads zero.
  • The give-back is credited with the completed write, after every charge the opcode makes and before the operation's positive effects are held, so it cannot cross a limit. A failure of the destroying frame, or of a frame containing it, discards the mark and every effect. A detention crossing at the opcode applies none of the table.
  • From the mark the account adds no further state growth or write record, and every later state charge on it, a store or the code a constructor deposits after destroying its own account, is priced at nothing, so it is neither charged nor refilled.
  • The neutral configuration counts the table and prices none of it, as EIP-8037 refills nothing for this destruction: equivalence mode reports Satin's own counts, so such a transaction differs there in usage alone.
  • On a journal that runs several transactions without finalizing between them, the facts read as "at the transaction's start" come from what the running transaction's journal records, never from a flag revm sets once per journal. A leaf is absent when its original info is empty and the database holds no account there or an earlier transaction on the journal destroyed it; an applied EIP-7702 authority counts as a new account exactly when revm charged it as one; and the keyless empty-deposit rule sees only the running transaction's destruction.
  • A node's database must answer an address it holds no account for as no account (None from Database::basic), never as an empty account: the table gives a leaf back only when no account stood at the address at the transaction's start.
  • The limit matrix's tests that waited for the table run; none is ignored.

Block execution and inspectors

  • Block execution admits any inspector: which inspectors may run on block production is the node's decision.
    • Removed: the TrustedObserver declaration, its DeclaredObserver carrier, MegaEvm::with_trusted_inspector, MegaEvm::has_rewriting_inspector, MegaBlockExecutorFactory::create_executor_with_trusted_inspector and MegaBlockExecutionError::RewritingInspector.
    • test_utils::ReadOnlyObserver remains for tests: in debug builds it checks that an inspector writes nothing back.
  • A handler that runs a second transaction reports that transaction's own reservoir, not one captured earlier.

Pre-block calls

  • Each pre-block call (EIP-2935, EIP-4788, applyPendingChanges()) runs on PRE_BLOCK_CALL_GAS_LIMIT = 31,566,720, whatever the block's gas limit: 30,000,000 of regular gas and a reservoir of sixteen fresh slots at the minimum bucket. One helper runs the three, and holds a fatal database error to an internal error and any other failure to a block verdict.
  • pre_block_call_gas_limit and MegaEvm::transact_system_call_with_gas_limit are removed.

Write records

  • An account's record exists while its nonce, balance or code differs from what the transaction loaded (revm's original info for the account).
    • A write that changes an unchanged account makes the record; a write that returns the account takes it back.
    • The per-frame deduplication, with its merge and restore code, is deleted.
    • A SELFDESTRUCT records the account it clears and its beneficiary.
  • A creator's nonce record is the creating frame's. It is counted on the creator once the creation returned with the nonce bumped, held to the creator's budget and then to the transaction's limit, and it survives the creation's failure; the creation's budget is the share taken at the opcode, before it. A keyless signer's record is counted on the call at its settlement, after the nonce set-back.
  • The frame-start hook only prices: it charges the history of the start's records and counts nothing. The records are counted once revm decided the start, and a creator's once its creation returned, so no count precedes revm's decision and none has to be given back.
  • A creation's start charges exactly the records it makes; the created account is charged only when the transaction has not changed that address already. The upper-bound charge, which gave its surplus back afterwards, is gone.
  • Debug builds hold the records each start counted to what revm's start made of the accounts: a call's sender and recipient, and a creation's created account.
  • A deposit's created caller is read off the account's original info.

Spec and engine aligned

Two engine changes and several specification changes.

  • A CREATE or CREATE2 loads its creator and its created address for its own checks. Those loads are not volatile reads, and are not refused in a frame whose volatile-data access is off.
  • A Satin key given twice in a genesis config object is read as its last copy by every reader.
  • satinTime is also read from the object {"$serde_json::private::Number": "<digits>"}, decimal digits alone, with no sign. The parser already accepted the object, and a test pins it; this states it in the spec.
  • A witness replay's exported buckets and block hashes must be a subset of the building node's, and may be a proper subset. This is a specification change only.
  • The specification defines a limit stop as the revert in exactly the four dimensions, and lists the inherited endings that are not stops: initcode over the size limit (a non-deposit creation transaction is rejected at validation, a creation opcode halts, a keyless deployment is refused) and deployed code over the size limit, which halts; an input over an OP precompile's size limit; a failed deposit; and a charge before the first frame the transaction cannot pay. A refusal that is not a limit stop never carries MegaLimitExceeded, and each sentence that returns unspent gas names the failed deposit as its exception.

The body's history in tx_gas

  • The transaction body's history gas is charged where revm builds the transaction's gas, as intrinsic gas paid from the reservoir first. Before, it rode in the intrinsic state-gas slot.
  • revm's runtime out-of-gas fallback rebuilds the gas through the same phase and keeps the charge. A test pins this on a transaction whose gas limit is above the 200,000,000 execution cap, the part paid from the EIP-8037 reservoir.
  • No outcome changes.

Names and public text

  • STORAGE_CALL_STIPEND_BYTES and storage_call_stipend() are renamed HISTORY_ALLOWANCE_BYTES and history_allowance_gas(), as the specification names the allowance.
  • The tests of the limit rules sit in tests/satin/limit_matrix/, one test per case of the rules.
  • The review guide, .claude/skills/satin-review/SKILL.md, describes the engine at this head. It is removed before feat!: the Satin execution engine on the revm 40 forks #386 merges.

mega-evme

  • A Satin run reports its state growth, and a stop names any of the four kinds (data_size, kv_updates, compute_gas, state_growth).
  • The three recorded blocks and their code store are one deterministic archive, tests/fixtures/blocks.tar.xz, extracted once per test binary; each file is moved into place on its own, so test processes that extract it at the same time all succeed. scripts/pack_blocks_fixture.py repacks it, and its --check requires a repack of the committed archive to be byte-identical. The published crate leaves out the scripts and the test fixtures.

The fork pins

  • All twelve revm crates point at https://github.com/megaeth-labs/revm and op-revm at https://github.com/megaeth-labs/op-revm, both by rev. Cargo.lock changes in their source lines, and eight packages resolve windows-sys 0.59.0 in place of 0.52.0 or 0.61.2.
  • docs/spec/overview.md names the two commits and their pull requests in place of release tags.
  • The two execution-spec workflows read the fork's revision from Cargo.lock whether it is pinned by tag or by rev.

Satin rule changes

Each of these changes what a Satin node computes, accepts or must check. Items 1 to 17 change receipts or state; items 18 to 22 change how a chain configuration is read, what a witness replay must hold, what a node's state must answer, and what a caller's configuration can change.

  1. An account's write record follows the net change of its info. The record exists while the account's nonce, balance or code differs from what the transaction loaded: N value transfers from one frame to the same recipient keep one record for the recipient and one for the sender, where they kept N; a transfer and its exact return keep none; a value CALLCODE, and a value CALL to the calling account itself, record nothing. Why: the record should measure state the transaction keeps, by revm's own notion of a change; the per-frame bookkeeping over-counted, and charged history gas for writes that leave nothing.
  2. A record that a value transfer takes back gives its history gas back to the caller, once the frame returned with its start kept. Why: what a transaction does not keep comes back at the price it was charged, as a storage slot written back already did.
  3. A SELFDESTRUCT records the account it clears, as well as its beneficiary. Why: clearing an account is a write the transaction keeps.
  4. A creation is charged its created account's record only when the transaction had not changed that address already (by funding it, or by a SELFDESTRUCT paying it). It is charged its creator's nonce record only when the bump changes the creator's account from what the transaction loaded. A keyless deployment charges by the same rule. Why: the net-change rule applied to creations; the old upper-bound charge made a creator that held enough for one record halt out of gas, and two Ethereum fixtures take a different path because of it.
  5. A creator's nonce record is counted on the creator when the creation returns with the nonce bumped, and the creation's budget is 98/100 of what the creator had left before it. The record is held to the creator's budget and survives the creation's failure; a keyless signer's record is counted on the call at its settlement. Why: the bump is a fact only once revm made it, and the creator's pools are in reach again only at the return, so nothing is counted before revm decides and nothing has to be given back.
  6. A creation onto an occupied address fails on the collision, and is no longer stopped for what its start would count. Its creator's nonce record is counted on the creator when the creation returns, and the creator is held to its limits with it. Why: a frame start is held to the limits once revm decided it — after the value moved and, for a creation, after the nonce was bumped and the created account made; the limit judges what the start did, not a prediction of revm's refusals, which duplicated revm's rules in the engine. For every other start, judging after revm decided gives the result the prediction gave.
  7. Work that a callee runs on a value call's 2,300 stipend counts as compute, and the stipend left over does not lower compute when it comes back to the caller. Why: the stipend is not forwarded gas, and the old figure let a callee spend it uncounted after a volatile read.
  8. What a halting frame burns is never compute, so a detained transaction's compute never exceeds the limit. A precompile that runs out of gas counts nothing of its forward. Why: the fork's failed-charge record tells exactly what a halting frame had left; the old exception, where a frame that could not pay an opcode's static gas counted what it had left as compute, overshot the limit by up to 4,999 gas per such frame, with no fixed total along a chain of frames.
  9. A CREATE or CREATE2 is neither marked as a volatile read nor refused in a frame whose volatile-data access is off, even when its creator or its created address is the block beneficiary. Before, such a creation in a frame with access off halted out of gas. Why: these loads are the creation opcode's own checks under EIP-8037, just as a value call's check of its caller's balance is not a read; Rex6 neither marked nor refused a creation.
  10. Each transaction is held to 500,000 write records and 1,000 new accounts and slots, and each frame to 98/100 of what its parent has left in each; a crossing is a limit stop of kind 1 or 3. Why: these are Rex6's limits, and gas does not stand in for the count: 1,000 fresh slots cost 97,920,000 state gas, which a gas limit can cover. At Satin's constants data size binds before the KV count, and the KV check is still made.
  11. An EIP-7702 list that would take the transaction over its KV or state-growth limit is skipped whole, and the transaction's own frame is stopped before it runs, naming the transaction limit (kind 1 when both would cross). Why: Rex6's all-or-nothing rule; Satin settles it as a revert-class stop instead of Rex6's halt, and a stop of the first frame does not revert what was applied before it, so applying the list would keep more than the limit allows.
  12. An operation that crosses several counted dimensions stops on the first of data size, KV updates and state growth, and the stop names the frame budget whenever that budget is crossed, the transaction limit only when it is not. Why: Rex6's order across dimensions, and a frame told the budget it crossed; compute is decided at the regular charge, before the write is counted.
  13. A frame that a failed callee returns into, over the transaction's data-size limit, returns that limit's stop before it runs another instruction, whether the callee reverted, halted or was itself stopped. This happens when the failure puts back a record of a frame above the callee that the callee had taken back, while an Oracle hint the callee sent into the room that freed stays counted. Why: a hint is on no frame and outlives the failure, so without the stop the transaction would keep more data size than its limit.
  14. A call or a creation forwards at most remaining − remaining × 2 / 100 of its caller's regular gas, and every child receives its caller's reservoir less 2 per cent, whatever gas the call asks for. The transaction's own frame and a system call's outermost frame take the whole reservoir. This departs from EIP-150's 63/64 and from EIP-8037, which passes the reservoir whole. Why: the regular forward is Rex6's, and state and history gas draw the reservoir first, so a callee forwarded the whole reservoir could spend all of it and leave its caller, which can catch its stop, nothing to carry on with.
  15. A block holds the KV count and the state growth to 500,000 and 1,000, as packing budgets beside its data size: the transaction that reaches one is included, and every later ordinary transaction is refused before it runs. Deposits and Mega System Transactions are never refused by them and count towards them; a failed deposit that creates its caller counts that account. Why: Rex6's block limits; a deposit is derived from L1 and must be included, and a Mega System Transaction is the sequencer's own update, so neither can be skippable on one budget and not on another.
  16. The first effective SELFDESTRUCT of an account created in the same transaction refills the state gas of each part the transaction made, to the destroying frame, lowers state growth for the leaf and each new slot, and takes back their write records and the code's data size and history gas; from the mark the account adds nothing and its later state charges are priced at nothing. Why: a part the transaction made does not remain for a node to store, so the count and the charge stay aligned; EIP-8037 refills nothing for this destruction and Rex6 refunded no storage gas, so this departs from both on purpose.
  17. Each pre-block call runs on 31,566,720 gas, 30,000,000 of it regular and the rest a reservoir of sixteen fresh slots at the minimum bucket, whatever the block's gas limit. Before, it ran on max(block gas limit, 30,000,000). Why: a pre-block call does not draw on the block's gas, the old budget was widened for storage gas a crowded bucket multiplied, which a system call no longer pays, a pre-block call on MegaETH's chains writes at most six fresh slots, and none of the three contracts reads GAS.
  18. A Satin key given twice in a genesis config object is read as its last copy by every reader. Before, a reader that saw both copies refused the file. Why: a node and the reference CLI parse the file into a JSON value, which keeps only the last copy, so refusing in one reader made readers disagree on the same file.
  19. satinTime is also accepted as {"$serde_json::private::Number": "<digits>"}, decimal digits alone, with the same range. The parser already accepted the object; the spec now says so, and a sign is refused. Why: that is the form serde_json hands a number over in when its arbitrary_precision feature is on, and a reader cannot tell it from the same object written in the file.
  20. A witness replay's exports must be a subset of the building node's, and may be a proper one. Why: the building node's exports also hold the buckets and block hashes asked for by candidates it executed and then dropped, which a replay of the included transactions never asks for.
  21. A node's state must answer an address it holds no account for as no account, never as an empty account. Why: the destruction table gives a leaf back only when no account stood at the address at the transaction's start, and an empty account is a leaf the state holds; a state that answers one for an address it does not hold executes a different chain.
  22. Satin's configuration fixes off the fork's new switch back to revm 40.0.3's journal semantics, whatever a caller passes. Why: the switch exists for the legacy engine, and no configuration a caller passes may change how Satin executes.

Breaking API changes

  • Removed for the inspector admission gate: TrustedObserver, DeclaredObserver, MegaEvm::with_trusted_inspector (use with_inspector), MegaEvm::has_rewriting_inspector, MegaBlockExecutorFactory::create_executor_with_trusted_inspector (use create_executor with an EVM from create_evm_with_inspector, or MegaBlockExecutor::new), and MegaBlockExecutionError::RewritingInspector.
  • Removed with the inspected copies and the rewrite settlement: settle_frame_result and with_pools_of.
  • Removed with the fixed pre-block budget: pre_block_call_gas_limit and MegaEvm::transact_system_call_with_gas_limit. constants::PRE_BLOCK_CALL_GAS_LIMIT is new.
  • Renamed: STORAGE_CALL_STIPEND_BYTES to HISTORY_ALLOWANCE_BYTES, and storage_call_stipend to history_allowance_gas.
  • StagedRecord and AdditionalLimit::staged_record are removed. AdditionalLimit::usage is now MegaContext::limit_usage.
  • LimitKind gains KvUpdate and StateGrowth, and LimitKind::from_u8 decodes 1 and 3. LimitUsage gains state_growth. EvmTxRuntimeLimits gains tx_kv_update_limit and tx_state_growth_limit, ProtocolLimits and BlockLimits gain block_kv_update_limit and block_state_growth_limit, each with its with_* builder. MegaBlockLimitExceededError gains KvUpdateLimit and StateGrowthLimit. constants gains TX_KV_UPDATE_LIMIT, TX_STATE_GROWTH_LIMIT, BLOCK_KV_UPDATE_LIMIT and BLOCK_STATE_GROWTH_LIMIT.
  • MegaGasUsage::new takes the reservoir remaining as an argument, since revm's ResultGas no longer carries it. AdditionalLimit::reservoir_remaining is new.
  • SYSTEM_CALL_REGULAR_GAS_LIMIT (30,000,000) is exported, now that Satin splits a system call's gas itself.
  • HISTORY_GAS_PRICED gains GasId::call_history_allowance.
  • SatinChainConfigError::DuplicateKey is removed.
  • test_utils::ReadOnlyObserver is new (test utilities only).
  • mega-state-test: the digest module (DigestForm, canonical, hash, text, panicked, Executed) and a re-export of EvmTxRuntimeLimits. state-test: the --json-digest, --json-digest-full, --limit-data-size, --limit-frame-data-size, --limit-kv-updates, --limit-state-growth and --limit-compute-gas options.
  • mega-evme: SatinReport gains state_growth, and LimitStop::kind may be kv_updates or state_growth.
  • The engine needs the fork's Host instruction hooks, its pools on GasTracker (PoolId), the reservoir share, the FrameStart / StartCharge carriers, the action a frame's run returns before it runs and the run-result hook: a downstream crate must build against the two fork commits below.

Dependencies

Verification

Run on macOS (aarch64).

  • Outcome differential. Each step was compared with the step before it by the outcome digest: equivalence mode, Satin mode and Satin under tight limits, on Osaka (19,465 transactions) and Amsterdam (15,754). The tight limits are 600 bytes of data size and a compute cap of 100, with three write records and two units of state growth added where a step says so. No run had a panic row.
  • Outcome changes during the work, each explained.
    • The write-record rules changed 1,512 transactions across the six runs. A classifier that recomputes each record count from the net-change rule explains every one, and catches a planted mutant that the consistency checks alone pass.
    • The exact creation charge changed two Osaka transactions, from test_dynamic_create2_selfdestruct_collision. A CREATE2 onto an address the transaction had funded no longer halts out of gas at its start.
    • The frame-start step at first charged a start that revm then refuses. Three Satin-mode rows, value calls whose caller cannot fund the value, halted out of gas. The record rules charge such a start nothing.
    • The detention and frame-start steps came before the digest and were compared by verdict: equivalence mode was identical, and Satin mode changed only those three rows.
    • The 98/100 regular-gas forward, whose source change is the one schedule entry, changed 820 Osaka and 655 Amsterdam transactions in Satin mode (359 and 354 under tight limits), and none in equivalence mode. A classifier sorts every one: a frame that burned its forward burned 98/100 where it burned 63/64 (gas only, less), a fixture that stores a GAS reading stored another, or a call that fit its forward under one rule and not the other; nothing else.
    • Naming the frame budget before the transaction limit changed 7,907 Osaka and 3,825 Amsterdam transactions under tight limits, every one only in the stop's limit word and the reported stop. Two oracle builds attribute each to the order alone.
    • The reservoir share, the KV and state-growth limits at the spec's numbers, the authorization-list skip, the block budgets, the inspected-lifecycle move and the loop hooks changed no outcome on the corpus. That the new stops run is shown under tight limits: three write records stop 1,489 Osaka and 1,722 Amsterdam transactions with kind 1, and one unit of state growth stops 1,544 and 3,248 with kind 3.
    • Counting the creator's nonce record at the return changed no transaction in equivalence or Satin mode. Under tight limits (600 bytes, compute cap 100) it changed 16 Osaka and 51 Amsterdam transactions, every one from the new order. A build that moves only the creation's share back to the old order reproduces the base on all 16 Osaka transactions and 50 of the 51 Amsterdam ones. The last is a CREATE2 from the EIP-7997 factory that the creator's record used to stop at its start: it now runs and returns, its creator stops at the return, and the transaction keeps the same usage and state but for fee balances, its gas differing by what the init code ran.
    • The destruction table changed 129 Osaka and 157 Amsterdam transactions in equivalence mode, in usage alone: the neutral configuration counts the table and prices none of it, so no result, ledger, log or state moves, and no fixture checks the counts. It changed 46 Osaka and 151 Amsterdam transactions in Satin mode, and 11 and 35 under tight limits (600 bytes, compute cap 100). A build with the table's three paths switched off reproduces the base on all six runs, so every difference is the table's.
    • The follow-ups after the table (the journal facts read off the running transaction, the resumed caller's stop pinned, the documentation) changed no outcome on any of the six runs; the state-test runner runs each transaction on a fresh journal.
  • Suites at this head. cargo test --workspace --release --locked: 1,843 passed, 0 failed. The three tests the last commits add, and the two they extend, pass at all 73 points of scripts/price_grid.sh --full. fmt, clippy (--workspace --lib --examples --tests --benches --all-features --locked, warnings denied) and Prettier on *.md, docs/**/*.md and the guide are clean.
  • Suites on the tree before the last two commits, which add three tests and reword module and guide docs:
    • cargo insta test --workspace --check --unreferenced=reject --test-runner cargo-test --no-quiet --fail-fast: 1,849 passed, 0 failed, no pending or unreferenced snapshot. cargo test --workspace --release --locked: 1,840 passed, 0 failed.
    • The fuzz target on seeds 0 to 8, release and debug: 23 passed each. mega-evme: 208 passed, 0 failed.
    • scripts/price_grid.sh --full: the suite passes at all 73 points, 1,555 tests each.
    • The 56 benches under --test: all pass.
    • A debug state-test in equivalence mode on the Osaka state tests: 19,465 executed, 19,463 passed, the 2 failures the two listed deviations, gate passed, no panic. In Satin mode: 19,465 executed, no panic.
    • cargo sort and the riscv64 no_std check are clean.
  • CI. The execution-spec workflows and the fail-closed mutation gate run in CI on this push. No local cargo-mutants run was made for this head.
  • Hand-placed mutants and red-first tests. Each behaviour change was checked, when it landed, with hand-placed mutants, every one killed by a named test: the net-change take-back, a record per write, a transfer's recipient priced on its old balance, the 98/100 share ignored, a crossing not turned into the stop, the start charge not refunded, the created-account charge in both forms, a SELFDESTRUCT's moved balance ignored, the body's history rebuild dropped, a creation's loads marked or refused as reads, the genesis parser's duplicate refusal put back, the forward quotient, the reservoir share, the three pool shares, the negative-net clamp, the frame-budget-first order, the authorization-list skip, the deposit's created caller, a SELFDESTRUCT beneficiary's rise, a value transfer's rise, the keyless signer's rise, the block KV budget, the exempt pools, the resumed caller's hold removed, the absent leaf's empty-original condition dropped and the applied authority's touch check dropped. Most of the creator-record and destruction-table tests were written red first, failing on the engine before the change; the rest pin behaviour the change keeps or creates.
  • Changed pins and generated files.
    • Snapshots: the keyless differential's digest, reviewed through its record dump at each change: the creator-record change moved the creation's share and the signer's record, and the destruction table moved the records of a constructor that destroys itself, each by exactly the deploy address's leaf and record. The tracer snapshots of the detention stop show the stop where they showed an out-of-gas. The corpus snapshot's self-destroying scenario went from 372,085 to 184,965 gas: the leaf's 183,600 and one record's 3,520.
    • Pricing table: the schedule entries above.
    • Deviation registry: unchanged.
    • Cargo.lock resolves revm 40.0.3 and op-revm 20.0.0 once each, from the two fork commits.
  • CodSpeed simulation. These are instruction counts (callgrind, CodSpeed's flag set) on an arm64 container, measured locally. CI runs x86-64, so read the relative moves, not the absolute counts. There is no measurement of the final head; CodSpeed's report on this PR gives that.
    • The first table's columns are this branch before the hooks (the failed-charge record adopted), with the hooks, and with the byte pool.
Benchmark before the hooks hooks byte pool end to end
block::transfers 2,658,155 2,658,596 2,747,863 +3.37%
block::storage_writes 1,789,430 1,789,551 1,810,041 +1.15%
block::steady_pre_block 46,294 46,258 47,117 +1.78%
corpus::sstore_set 35,467 35,341 34,589 −2.48%
corpus::create 49,908 49,837 51,513 +3.22%
corpus::selfdestruct 46,274 46,239 49,183 +6.29%
transact::empty_transaction/satin 23,437 23,411 23,761 +1.38%
transact::ether_transfer/satin 24,176 24,150 25,726 +6.41%
transact::storage_writes/satin 397,409 396,378 406,505 +2.29%
transact::logs/satin 215,564 217,337 216,941 +0.64%
transact::deep_calls/satin 453,321 457,139 474,716 +4.72%
transact::deep_calls/op_revm 360,022 360,260 374,540 +4.03%
transact::intercepted_calls/satin 470,405 489,780 529,963 +12.66%
transact::value_calls/satin 718,478 729,653 1,033,977 +43.91%
transact::value_calls/op_revm 527,677 528,119 558,630 +5.87%
transact::salt_new_accounts/satin 103,252 103,974 127,896 +23.87%
  • The op_revm rows run op-revm's own EVM on the same workloads, and their code did not change. Their +3.5% to +6% on call-heavy benchmarks is the fork's new call path: larger frame results, the forward share and the start charge.
  • On the Satin side, the frame-start side costs most: a value call's records are read twice, once at the hook and once where revm decides the start, and the net-change rule reads and clones the accounts' original info. The write side got cheaper.
  • Restoring the KV and state-growth limits with the reservoir share, measured before and after on the same rig, moved the op_revm rows of the frame-heavy benchmarks (deep_calls, intercepted_calls, system_address_misses, remaining_compute_gas, value_calls) by +8.5% to +13%, the fork's cost of forwarding and settling three pools and the reservoir share at every start, and the same Satin rows by +17% to +20%; storage_writes, logs, data_size_limit and salt_new_accounts moved +12% to +18% on the Satin side.
  • Nothing was optimised in this PR (see Follow-ups).

Follow-ups

These stay out of this PR.

  • Performance. Optimisations are recorded and done together once the structural work is finished, measured against CodSpeed's report on this PR. Candidates: the double read of a start's records, the account-info clones of the net-change rule, the fork's larger frame results, the per-start work on three pools, and the second signature recovery of an authorization list that may cross a limit.
  • Pins. The fork pins move to the merged commits of revm#76 and op-revm#8, and to tags once the line is final.

Labels

spec:unstable, api:breaking, comp:core, comp:mega-evme, comp:misc, comp:doc, dependencies

# Conflicts:
#	AGENTS.md
#	crates/mega-evm/tests/_pending/README.md
A transaction of the system shape (a legacy call to a whitelisted
contract) reads the SequencerRegistry's current system address from the
journal, without warming it, when it is validated. Every other
transaction reads nothing and is never a system-address transaction.

The block executor no longer reads or plumbs the address. Removed:
MegaContext::{system_address, set_system_address, with_system_address},
system::resolve_system_address, system::sent_from_system_address,
PreBlockStateSource::SystemAddress and the three MegaBlockExecutionError
variants only that read produced. A transaction from the system address
of another shape now runs as an ordinary transaction instead of being
refused.
@RealiCZ RealiCZ added the dependencies Pull requests that update a dependency file label Oct 22, 2026
@mega-maxwell

mega-maxwell Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Claude review status

Living comment — rewritten in place. The review workflow keeps this single comment up to date instead of posting a new one each round, so it always describes the latest reviewed commit and the earlier text is intentionally gone. No reply is needed here; reply to a finding in its own review thread, and answer an open question in a reply on this PR. The next review round reconciles your answer.

🛠️ Review did not finish

Attempted f325056e..0d9a92e8 · updated 2026-10-23T01:08:17+00:00

This round did not publish: MODEL_ACTION_FAILED in phase review_retry. Anything listed below is from the last round that did. Re-run the workflow or push a new commit to try again.

@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.21384% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.8%. Comparing base (85a418d) to head (0d9a92e).
⚠️ Report is 1 commits behind head on cz/feat/satin-w2.

Files with missing lines Patch % Lines
crates/mega-evm/src/evm/host.rs 95.7% 1 Missing and 4 partials ⚠️
crates/mega-evm/src/limit/record.rs 99.3% 1 Missing and 2 partials ⚠️
crates/mega-evm/src/limit/mod.rs 98.3% 1 Missing ⚠️
crates/mega-evm/src/system/keyless/settle.rs 94.4% 0 Missing and 1 partial ⚠️

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codspeed

codspeed Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 11.58%

2 warnings

⚠️ 12 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 28 regressed benchmarks
✅ 27 untouched benchmarks
⏩ 389 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ value_calls/satin 485.9 µs 727.3 µs -33.2%
❌ remaining_compute_gas/satin 294.5 µs 367.9 µs -19.93%
❌ intercepted_calls/satin 295.2 µs 368.1 µs -19.82%
❌ storage_writes/satin 355.2 µs 435.7 µs -18.48%
❌ system_address_misses/satin 646.4 µs 791.8 µs -18.36%
❌ salt_new_accounts/satin 120.2 µs 146.5 µs -17.97%
❌ salt_new_accounts/crowded 120.7 µs 146.7 µs -17.69%
❌ deep_calls/satin 449.2 µs 535.1 µs -16.04%
❌ data_size_limit/satin 367.4 µs 430.2 µs -14.62%
❌ data_size_limit/stopped 419.7 µs 486.1 µs -13.67%
❌ logs/satin 169.5 µs 191.8 µs -11.66%
❌ value_calls/op_revm 360.5 µs 407.9 µs -11.62%
❌ selfdestruct 88.4 µs 99.8 µs -11.39%
❌ remaining_compute_gas/op_revm 516.5 µs 573.6 µs -9.96%
❌ deep_calls/op_revm 373 µs 414.1 µs -9.92%
❌ system_address_misses/op_revm 527.3 µs 584.8 µs -9.84%
❌ intercepted_calls/op_revm 532 µs 589.1 µs -9.69%
❌ create 88.8 µs 96.9 µs -8.39%
❌ oracle_reads/service 218.1 µs 237 µs -7.96%
❌ salt_storage_writes/satin 86.4 µs 93.8 µs -7.93%
... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing cz/feat/satin-w9 (0d9a92e) with cz/feat/satin-w2 (85a418d)

Open in CodSpeed

Footnotes

  1. 389 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 720ad2435d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 228 to +229
if is_satin_key(&key) {
let entry = map.next_value::<Entry>()?;
if entries.contains_key(&key) {
duplicate.get_or_insert_with(|| key.clone());
}
entries.insert(key, entry);
entries.insert(key, map.next_value::<Entry>()?);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore superseded duplicate values before decoding them

When duplicate satinTime keys are streamed, every occurrence is deserialized as Entry before the last one replaces it. Consequently, a superseded value such as {"$serde_json::private::Number": 1} fails at next_value::<String>(), while parsing the same config through serde_json::Value discards that occurrence and accepts a later valid satinTime. This defeats the intended last-copy semantics and makes readers of the same genesis disagree; buffer or skip earlier duplicates so only the final value is interpreted.

AGENTS.md reference: AGENTS.md:L224-L224

Useful? React with 👍 / 👎.

RealiCZ and others added 2 commits October 22, 2026 10:50
(cherry picked from commit 821577d)

Conflict resolution:
- .github/workflows/claude.yml: main's version, unchanged.
- .github/workflows/coverage.yml: this branch's version, which already
  uploads over OIDC with fail_ci_if_error, continue-on-error and a
  warning step, and triggers on satin; main's persist-credentials: false
  is added to its checkout step.
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

🧬 Mutation testing — ❌ FAIL

Diff mutation score: 93.7% (283/302 viable mutants killed)

  • caught: 283
  • survived (real gaps): 19
  • timed out (inconclusive): 1
  • suppressed (equivalent/dead-code): 0
  • unviable: 309 · timeout total: 1
  • re-checked alone after timing out: 1 — 0 caught, 0 survived, 1 timed out again, 0 not re-run

Survivors needing attention

Each mutation below changed the code but no test failed. Add a test that kills it, or — if it is provably equivalent/dead — add a justified entry to mutants/suppressions.toml.

  • crates/mega-evm/src/evm/execution.rs:958:18: replace < with <= in cap_the_shares
  • crates/mega-evm/src/evm/execution.rs:1518:45: replace && with || in simulate_authorization_list
  • crates/mega-evm/src/evm/execution.rs:1531:31: replace || with && in simulate_authorization_list
  • crates/mega-evm/src/evm/execution.rs:1534:25: replace += with *= in simulate_authorization_list
  • crates/mega-evm/src/limit/limit.rs:208:30: delete field transfer_log from struct Self expression in Counted::plus
  • crates/mega-evm/src/limit/limit.rs:410:66: replace - with + in AdditionalLimit::authorization_list_may_cross
  • crates/mega-evm/src/limit/limit.rs:412:59: replace - with + in AdditionalLimit::authorization_list_may_cross
  • crates/mega-evm/src/limit/limit.rs:413:42: replace > with >= in AdditionalLimit::authorization_list_may_cross
  • crates/mega-evm/src/limit/limit.rs:413:71: replace > with >= in AdditionalLimit::authorization_list_may_cross
  • crates/mega-evm/src/limit/limit.rs:456:32: replace < with == in AdditionalLimit::first_frame_pools
  • crates/mega-evm/src/limit/limit.rs:456:32: replace < with <= in AdditionalLimit::first_frame_pools
  • crates/mega-evm/src/limit/limit.rs:458:28: replace > with == in AdditionalLimit::first_frame_pools
  • crates/mega-evm/src/limit/limit.rs:458:28: replace > with >= in AdditionalLimit::first_frame_pools
  • crates/mega-evm/src/limit/limit.rs:464:37: replace < with <= in AdditionalLimit::first_frame_pools
  • crates/mega-evm/src/limit/limit.rs:545:21: replace > with >= in AdditionalLimit::check
  • crates/mega-evm/src/limit/limit.rs:597:16: delete ! in AdditionalLimit::apply
  • crates/mega-evm/src/limit/limit.rs:853:21: replace > with == in positive
  • crates/mega-evm/src/limit/limit.rs:853:21: replace > with >= in positive
  • crates/mega-evm/src/limit/record.rs:283:23: delete ! in Accounts<'a>::destroyed_parts

Timed-out mutants (inconclusive)

Each mutation below never finished within the timeout, so it was not proven caught. Make it terminate (a faster test), kill it, or — if it is a genuine non-terminating/equivalent case — add a justified entry to mutants/suppressions.toml.

  • crates/mega-evm/src/limit/limit.rs:465:19: replace += with *= in AdditionalLimit::first_frame_pools

Tip: run /improve-mutation-score to triage and fix these.

@mega-maxwell mega-maxwell Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Review needs attention — 1 finding(s)

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

Reviewed head 9c1909a6.

Findings without inline anchors:

  • crates/mega-evm/src/block/genesis.rs:308 — [Minor] satinTime readers diverge when the first duplicate value is a malformed arbitrary-precision form Two honest readers of the same genesis file disagree on validity — the streamed path refuses it, the Value path accepts the last satinTime. The trigger is contrived (serde_json never emits a non-string arbitrary-precision form), so natural files are unaffected, but it violates the newly stated spec rule (PR §10) that every reader reads the last copy. The existing test test_the_last_copy_of_a_key_given_twice_is_read only exercises number/string first copies and does not catch this. Suggested fix: Buffer or skip earlier satinTime copies before decoding them in EntriesVisitor so only the final occurrence drives the type check. One shape: iterate with IgnoredAny and record the last serde_json::Value for each key, then decode only that into Entry after the loop. Add regression tests with {"$serde_json::private::Number": 1} and other malformed objects (e.g. [1]) as the first duplicate so the streamed and Value paths are pinned to agree.

@RealiCZ
RealiCZ added this pull request to stack #408 October 22, 2026 06:45
@RealiCZ
RealiCZ removed this pull request from stack #408 October 22, 2026 06:47
Base automatically changed from cz/feat/satin-w6 to cz/feat/satin-w5 October 22, 2026 06:48
Base automatically changed from cz/feat/satin-w5 to cz/feat/satin-w4 October 22, 2026 06:49
Base automatically changed from cz/feat/satin-w4 to cz/feat/satin-w3 October 22, 2026 06:49
Base automatically changed from cz/feat/satin-w3 to cz/feat/satin-w2 October 22, 2026 06:49
cz/feat/satin-w2 now holds W3 to W7 as squashed commits; this branch is built
on #399's final commit (4912ae7), whose tree equals that branch's tip. The
merge keeps this branch's tree unchanged.
RealiCZ added a commit that referenced this pull request Oct 22, 2026
…hanges

The stack merged down into #386, and the skeleton (#385) merged into
`satin` first, so the guide now names #386 as the pull request under
review and `a8f8c7c9..<head>` as the range that still holds the
skeleton.

- A table of the engine's modules says which are stable and which #407
  still changes, derived from the diff of `cz/feat/satin-w9` against
  this branch and the work still to land in it; it replaces the old
  "What follows" section.
- The fork bullet keeps this branch's tag pins and names the fork pull
  requests that review the commit pins #407 moves to.
- Module headings, three checks and the decided items #407 revisits
  say so.
RealiCZ added a commit that referenced this pull request Oct 22, 2026
Brings the review guide's update for #386. The guide's four conflicting
passages keep this branch's facts (the forks pinned by rev, block execution
admitting any inspector); the rest of the update merges as is and is
refreshed for this branch's code when it merges into cz/feat/satin-w2.
Brings the review guide's revert-or-halt section.
…cles, count the creator record at the return and add the destruction table

- Restore the KV-update and state-growth limits per transaction, per frame
  (98/100 shares) and per block; a crossing is a revert-class limit stop of
  the frame that crosses it, ordered data size, KV updates, state growth,
  the frame budget named first.
- Forward the reservoir and regular gas at 98/100 on the forks' pools, and
  replace the storage-gas stipend with a 160-byte history allowance.
- Run frames on revm's own plain and inspected lifecycles through the forks'
  loop hooks; no inspector-specific code remains in the engine.
- Count a creator's nonce record on the creator when the creation returns
  with the nonce bumped; the creation's budget is the share taken before it.
- Give back what a transaction made of an account its first effective
  destruction removes (the same-transaction destruction table), read from
  the journal.
- Read an account's absence at a transaction's start from that
  transaction's journal.
- Pin the forks at revm 2fc8edf33b2ffddb195cebc17be9adabfd95417c and
  op-revm f599efdabc3e5b36a280f1aa72978751bd64c160.
- Accept a limits file of the first published shape; pack mega-evme's
  recorded block fixtures into one archive; specification, implementation
  notes and the review guide updated to the final engine.
@RealiCZ
RealiCZ merged commit be2a56f into cz/feat/satin-w2 Oct 23, 2026
25 of 28 checks passed
@RealiCZ
RealiCZ deleted the cz/feat/satin-w9 branch October 23, 2026 02:25
RealiCZ added a commit that referenced this pull request Oct 23, 2026
RealiCZ added a commit that referenced this pull request Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api:breaking Crate interface change — downstream users must update comp:core Changes to the `mega-evm` core crate comp:doc Changes in the documentation comp:mega-evme Changes to the `mega-evme` tool comp:misc Changes to the miscellaneous part of this repo dependencies Pull requests that update a dependency file spec:unstable Changes to the unstable spec (currently REX5)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants