Repository navigation
feat(satin): node integration API, the stateless witness, randomized tests and four engine fixes - #400
Draft
RealiCZ wants to merge 282 commits into
Draft
feat(satin): node integration API, the stateless witness, randomized tests and four engine fixes#400RealiCZ wants to merge 282 commits into
RealiCZ wants to merge 282 commits into
Conversation
An SLOAD in the Oracle's own frame loads the chain's slot and takes the oracle service's answer over it. The answer is in no database, so a stateless validator must be given it, and a node's own recorder around the service sees every candidate the builder executed, dropped ones included, which puts answers in the wrong order for a validator that runs only the included transactions. The context now records every read the service is asked, with its answer, in an OracleReadRecord that belongs to one transaction: it is emptied before every transaction and system call, and the transaction's outcome carries a copy. A node takes the reads of the transactions it includes from their outcomes and nothing of a dropped candidate. A read a frame cannot pay for, or one it is refused, asks nothing and records nothing. No execution result changes: the record is written where the service was already asked, and decides nothing.
… miss The bucket record was written before the environment was asked, on every ask, cache hits included. Two things were wrong with that. A lookup that failed was exported, though it fails its transaction, which is in no block: a validator never makes the lookup, and a builder whose SALT backend failed on a bucket was held to proving it. And the record was written on the hot path once per charge rather than once per bucket per transaction. The bucket is now recorded on the cache miss alone, once the environment answered its capacity, as the legacy engine recorded it. The multiplier cache splits its lookup from its fetch so the context can tell a miss from a hit with one lookup on the hit path. The set a block exports is the same for every transaction that is in a block; a failed lookup is no longer in it. The export's coverage is stated on both accessors: what one EVM executed since the block started, dropped candidates included; nothing of an outcome executed on another EVM and committed here; the union is the node's to take when it executes on several EVMs or clears the record between transactions; and an EVM reused across blocks outside a block executor is emptied by neither. No execution result changes: the record decides nothing, and the multiplier each charge is priced with is the one it was.
The minimal cases carry the gas limits they were found with, and the charge each falls short on moves with the prices; at other prices the tests return early with the note the byte-price grid counts, as the crate's own tests do.
The harness replayed a block on the record of every read its database served. That is a real check of determinism and of hidden inputs, but not of the witness a node builds: a node collects keys from the pre-block states and the included transactions' returned states and resolves them against its own state, and a read the engine makes outside every state and export is a key such a witness lacks. Nothing in the harness could see one. The test utilities gain the channel witness: WitnessKeys collects what the states name, and WitnessRecord::from_channels resolves the keys against the pre-block database, takes the block hashes and buckets from the exports and the oracle answers from the included transactions' own records. Every harness case and the state-test check now replay on it too, and pass; with the pre-block L1 block info entry taken out, every case with a user transaction fails on it. Both replays run the transactions the recorded block included and only those: a candidate the builder executed and dropped, which a case can now express, and a transaction the block refused, are no validator's to run. The comparisons follow: exports and the state cache are equal when every candidate was included and otherwise the replay's are among the recording's, and a transaction's oracle reads are the same reads on both runs while their answers are the service's. New cases: a dropped candidate, a SALT lookup that fails, the oracle answers a validator replays being the included transactions' own and not the service's log, and a validator without a service needing the included update. The two oracle skip cases run on a chain that holds the Oracle, with the read they skip shown to happen otherwise, and the pre-block half of the bucket test writes a slot through the EIP-2935 code before asserting no bucket was asked. The idle imports are gone.
The section defined the witness as what a recorder at the database level sees, which is not what a node builds and is not well defined under a state cache filled by an earlier block, and it carried a special case for the L1 block info and function names and implementation terms the spec rules forbid. The witness is now the accounts and slots the pre-block states and the included transactions' returned states name, as the chain held them, with their code; the block hashes and SALT buckets the block's execution exports; and the oracle answers the included transactions recorded. The set is a function of the block's states and exports alone. The L1 block info is a pre-block state like the registry's pending slots. The bucket set holds answered lookups only, dropped candidates' included, and a node running several executions takes the union. The oracle contract names the records a node takes from the transactions it includes and, for a validator without a service, the value the journal holds at the read as the included transactions left it. The slot number is stated as an input the validator is given beside the header. The glossary entry follows, and the AGENTS.md test-gate section counts its four gates and describes the two replays.
…e empty The pre-block read of the L1 block info delivered the overhead slot whatever the scalars held, on the reasoning that a set independent of the state it describes is simpler. op-revm reads the overhead only when the Ecotone scalars are empty, and a node that replays a block from a recording of the block's reads finds no overhead in it: the tool's offline replay of the recorded mainnet blocks failed on that slot. The read now mirrors the pricing's rule: the four slots always, and the overhead when the scalars word it read is empty, decided as the pricing decides it. The witness holds exactly what the block read, and the spec says so. Execution results do not change: the entry is read-only either way.
The mutation gate found no test calling MegaEvm::oracle_reads: the accessor could answer an empty slice and nothing would fail. The Oracle storage test that pins the outcome's record now pins the EVM's too.
…oolchain action's cache
…s cleared A failed deposit's settlement reset the whole count to the body, so an Oracle hint the deposit forwarded before it halted left its data size, where the same program reached by an ordinary transaction keeps it. The settlement now clears the latch the body may have set and nothing else.
…-precision number object as the number
…n it loads the configuration
…a transaction's start
…e limit A hint whose bytes would take the transaction over its data-size limit is not forwarded, and it was counted all the same: the stopped transaction reported the payload as data size it kept, and the block's data-size counter took it, though the payload never left the node. The limit is now checked before the bytes are counted, so the stop reports the usage the hint would have reached and the transaction keeps none of it. A forwarded hint stays counted.
… the fee and for a self-sponsored delegation Two false positives of the property on what a stop keeps: an applied authority that is also the block beneficiary is credited the fee, and a sender that delegates itself keeps the state gas of its delegation though it makes no record.
…on inside init code The neutral flavor replaced one init-code shape and left the others: a creation transaction's own init code ending in SELFDESTRUCT, a nested init program, and code either borrows through CALLCODE or DELEGATECALL. A contract destroyed in the transaction that created it is settled by EIP-8246 on Amsterdam, which the Osaka base does not have, so those cases differed from Ethereum there. The rewrite is now the case's, applied to the Amsterdam run of the Ethereum arm alone; op-revm and Ethereum on Osaka are compared on the case as drawn, the direct self-destructing init code included. The test prints how many cases the rewrite changed.
The expected block usage was read back from the block's limiter after every commit, so the property compared the limiter with itself. It now adds what each committed transaction's outcome reports.
The channel witness took the code the returned states carry beside the code the chain holds for the accounts they name. The first is not what a node proves, and it is not what the engine loads either: a transaction that replaces an account's code, an EIP-7702 authority delegated anew, is admitted against the code the chain held, while its returned state carries the code it wrote. A witness built from the states' code alone fails 9 Osaka and about 70 Amsterdam fixtures on exactly that read. The witness keys are now accounts and slots only, and the code is the chain's for every named account, asked by hash from a chain that serves it that way. A harness case pins the replaced delegation, and unit tests pin what the channel witness holds.
The oracle cases dropped a candidate that read another slot, or wrote the slot. The case the contract is written against is a candidate that read the same slot and was answered another value, the service having moved on before the included transaction read it. The harness lets the service change an answer once a candidate was executed. The new case pins both options on it: a validator given the included transaction's record replays the block, one handed the service's log answers the read with the candidate's value for the same slot, and a validator without a service computes the block exactly when the slot holds the included read's answer at the read.
A returned state names the keys of the witness; its values are the chain's. For code the difference decides a replay: a transaction that replaces an account's code is admitted against the code the chain held, and its returned state carries the code it wrote.
AGENTS.md counts the witness replay among the repository's execution gates and points at REVIEW.md for every check a pull request meets, which did not list it.
…the survivors property A raw call of the Oracle takes its calldata from memory as it stands, which may hold the sendHint selector with a payload that does not decode. Such a hint is admitted and counted, as every hint is before it is decoded, and reaches nobody, so the hints the oracle service recorded are a lower bound on the hints counted wherever a program can send one, and exact elsewhere.
… them to their rules A keyless case drew its call's value and gas from the generators every shape shares, signed at a wrong nonce three times in eight, passed a zero override three times in eight, and always met a signer with no nonce and no code and an empty deploy address: about one in twenty started its creation, and four of the rules could not be reached at all. The shape now carries the signer's account (funds, nonce 0 to 2, plain code or a delegation), what the deploy address holds (nothing, a contract, a balance, a nonce), the encoding of the carried transaction (valid, or one of five the rules refuse), the signed gas limit, and draws the call's value and gas from generators of its own. Init code can call its caller, which in a keyless creation is the signer, so a delegating signer's code runs and spends nonces. A property of its own draws keyless cases alone, restates the rules from the generator's fields in the spec's order, and holds the refusal, the answer and the signer's nonce to them. It prints how often each rule was reached.
Forwarding all gas let a creator re-enter itself hundreds of levels deep on the larger gas tiers, which the debug run paid for in seconds. Two million gas is room for the caller's code to create once more and no deeper than a few levels. The keyless property runs 512 cases in the bounded mode, where each rule is still reached.
…ansaction Through the block executor's own commit, a deposit and a user transaction whose bucket read fails each return an internal error naming the transaction and the cause, and the block commits nothing for either: no receipt, no counter, no nonce bump, no mint, no write, no bucket. The block goes on once the user transaction is left out.
A deployment whose initcode returns empty code leaves the address with nonce 1 and no code; a second deployment there passes the code check and collides: ExecutionHalted with the whole forward as gasUsed, the signer kept at nonce 1, the address unchanged, 132,000 plus the forward beyond a plain call, and no state or history gas. The least gas limit that deploys is pinned by hand, and one gas less is refused.
A factory creates a child whose constructor destructs, then sends the destroyed child value. The receipt carries exactly the three transfers and no log for the burn, no Burn log either; across every account the transaction touched the balances fall by the late value, and once the block finishes the child is gone.
…a node does Both replays built the block context without the activation-block switch, so they accepted a user transaction in a block in which a fork activates. The block replay now works it out from the schedule the block runs under and the parent block's timestamp, which it fetches with the block and keeps in the block cache; an entry without it gets it over RPC, and offline a block whose answer depends on it fails rather than guess. The transaction replay works it out from the parent block it forks.
…sits The blockchain runner imports blocks through the block executor under a schedule holding every Ethereum, Optimism and MegaETH fork, and built their context without the activation-block switch. Work it out from the schedule and the head's timestamp, as a node does. The runner's schedule activates every fork at genesis, so no block it imports is an activation block, and the outcome of every fixture is unchanged.
Clippy flags the inline tuple type of the genesis check's cases as too complex.
The collision test took its gas figures as the difference from a reference transaction run on the same engine. Assert each tier's regular, state and history gas, history bytes and total as absolute values, from the calldata, the body's byte count and the configured byte prices.
Loading a cached entry without the parent's timestamp fetched the parent's header over RPC for every block, the legacy engine's included, and failed the replay when the endpoint did not have it. Fetch it after the engine is chosen, only for a Satin block whose schedule makes the activation-block rule depend on it, so every other block replays from the cache alone.
…che lacks A Satin block whose activation-block answer depends on its parent's timestamp, replayed offline from an entry without it, still fails rather than guess. Its error and the replay docs now say how to recover: one replay with --rpc fetches the timestamp and writes it to the block cache, and the block then replays offline.
…hanged test Add the rule tag and the provenance line to the replay helper's two unit tests, the state-test runner's import test and chain-spec test, the predeploy-list and empty-account deploy tests, and the cached-RPC block replay test.
…ention overshoot, the exempt floor, pre-frame precedence
… predeploys over storage, failed reads at block level, keyless and burn edges
At a history price of 1,000 per byte the fixed 5,000,000 did not cover the history the factory pays for the records of the two frames it starts, from the 64th of its gas it keeps after each forward, and the transaction ran out of gas. Build the limit from the configured prices, as the file's other tests do.
RealiCZ
added this pull request to stack #403
October 12, 2026 12:39
…the creation pays before the later call The later value call charges one record, since the factory's own is deduplicated in its frame, and the creation has returned its unused gas by then, so the two 64-fold reserves never coexist.
…, so the release tests build
… catchable stops, validation rules, tools and cleanup (#404)
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Satin gains the API a node, a stateless validator and a transaction pool integrate it through, a definition of the stateless witness with a harness that replays blocks and the execution-spec fixtures from it (the fixtures in CI), randomized tests with the four engine fixes they found, golden tests of the tracer output a node serves, the execution-spec blockchain tests run through the block executor as a gate, and the CI and test coverage the engine was missing. Stacked on #399 (
cz/feat/satin-w6).spec_cfgexports the configuration Satin runs on;validate_transaction_statelessvalidates a transaction for a pool without state and reports its intrinsic gas by ledger;LimitStopreads a limit stop off a result for an RPC error;SatinChainConfigis the one parser of a genesis file's Satin keys, andmega-evme --genesisruns a chain from its genesis file. The workspace declares and checks Rust 1.94.fuzztest target of 20 tests onproptest: properties the engine must hold on any input and a neutral differential against op-revm and revm's mainnet EVM. They found four engine bugs, fixed here.tracerstest target of 15 tests pinned withinsta: the JSON of callTracer, prestateTracer and the opcode tracer, built as a node builds them, and the EIP-3155 trace, pinned for transfers, storage, creations, keyless deployment, limit stops within and across frames, detention, the reservoir, a precompile and deposits. Each scenario also holds the output to relations that do not depend on the snapshots: gas worked out from the schedule, and the prestate against the reads the engine made.state-test btestgate: every Osaka test of the execution-spec release'sblockchain_testsimported block by block throughMegaBlockExecutorin the neutral configuration, each block held to its header's gas used, logs bloom, receipts root and state root. 18,713 tests run, 20,232 blocks checked against Ethereum, 0 unattributed.insta(a test-only dependency) pins the tracer output and a per-case digest of the keyless differential record; behaviour tests keep explicit assertions and carry no snapshots.CI and test coverage
build-and-test.ymlgainstest-release(cargo test --workspace --release --locked, which runs the randomized tests' bounded mode in release, where debug assertions are gone),system-contracts(forge testincrates/system-contracts) andmsrv(below).cargo test --workspacethroughcargo insta test --check --unreferenced=reject, once: a snapshot that differs, a snapshot file no test compares, or a pending.snap.newin the tree fails it. The keyless differential test pins one keccak256 digest per case of its record (155 cases), which only a person compared by hand before;MEGA_KEYLESS_RECORD=<file>still writes the full record for review. Snapshots pin only outputs with no computable expectation; the rule is inAGENTS.md.mutation.ymlloses the two spec-gate jobs: a single spec leaves that operator pack nothing to mutate, and the nightly sweep still named the removed sandbox subsystem. The pack stays on disk, and the suppression hygiene check still accepts an empty plan.exec-spec-satin.ymlgains a step that replays every Osaka and Amsterdam fixture entry from its witness in both modes and fails the gate on any entry that does not replay, and on a run without exactly one well-formed summary, that executed nothing or failed an entry (the fixtures section below). It adds about 35 s locally.exec-spec-satin.ymlalso extracts the release'sblockchain_testsand gates them withstate-test bteston every pull request (the blockchain-tests section below).exec-spec.ymlsays why it runs on a pull request only when the manifests or the workflow change: it runs the revm fork's own runner at the locked revision, which no engine change moves.MegaGasUsage::gas_used, and the header's gas used is the final sum.CREATEfollowed bySELFDESTRUCTin the same transaction keeps the created account's state gas at bucket multipliers 1, 2 and 8: the account is marked self-destructed, and the charge is not refilled.SequencerRegistryrotation taking effect across blocks, the EIP-2935 history buffer wrapping, and a user transaction in the block after an activation block.KeylessDeploy,MegaAccessControlandMegaLimitControl. No contract source and no generated bytecode changed.tests/satin/spec_numbers.rsreadsdocs/spec/upgrades/satin.mdand checks the prices, caps, limits and addresses it states against the constants and the schedule at the spec's own byte prices.Node integration
rust-version = "1.94"(it declared 1.94.1). Only the lock moves for it: the alloy 2.x crates go from 2.4.2 to 2.1.1, the newest line that does not declare 1.94.1. Themsrvjob checks the workspace, every feature on, and theno_stdtarget on rustc 1.94.0; both pass on 1.94.0 here..cargo/config.tomlsets the resolver'sincompatible-rust-versions = "fallback", so acargo updatein the repository keeps the lock on releases 1.94 builds.spec_cfg, exported. The configuration the spec fixes (the Satin gas schedule, EIP-8037, EIP-2780, EIP-7708, the 200,000,000 execution cap, the code-size limits). A node builds itsEvmEnvthrough it, so gas estimation, which reads the env before an EVM exists, reads what the EVM executes on.validate_transaction_stateless, for a pool. It runs the handler's ownvalidate_envandvalidate_initial_tx_gason an EVM over an empty database and returnsIntrinsicGas { regular, state, history, floor }.MegaHandler::validate_envis split at the state read of the live system address (validate_env_as), so the pool and the handler share every other step; the pool passes the address in, read withsystem::live_system_address(db)from aDatabaseor, throughWrapDatabaseRef, aDatabaseRef.IntrinsicGas::min_gas_limit()is what validation requires, not what a transaction needs: at it, a value transfer to another account runs out of gas on its recipient's record.FailedDeposit(sender's nonce bumped, mint credited, whole gas limit used, nothing else applied), so for a deposit, or a system-address transaction once promoted, the helper's error means the transaction would fail. The Satin page's refusal section now agrees with its node-integrator section and the engine: a Mega System Transaction's own chain-id, nonce and EIP-3607 checks stay a rejection, made before it runs as a deposit, and the skip list's block-limit items carve deposits out.LimitStop, for RPC errors.MegaTransactionOutcome::limit_stop()andLimitStop::from_result(result, limit_exceeded)give the kind, limit and usage of the limit that stopped a transaction;decode_mega_limit_exceededreads revert data alone, and bytes that decode are not a stop by themselves.AdditionalLimit::latched()is reset when the next transaction or system call starts; the outcome'slimit_exceededis captured when the transaction ends.SequencerRegistryConfigin JSON, inProtocolLimits' shape: camelCase, every field required, unknown fields refused.SatinChainConfig, the one parser of a genesisconfigobject's Satin keys:satinTimeand one flatsatin-prefixed key per registry seed and protocol limit (sixteen). It refuses a missing, unknown (any key starting withsatin, in any case) or mistyped key, and a key given twice where the deserializer still shows both copies (mega-evme --genesisand a node parse into aserde_json::Valuefirst, which keeps the last); it validates both params types and the schedule, serializes back to the same keys, and reads a number serde_json'sarbitrary_precisionrepresents as a map. It reads no other fork's key: a node checks at load that each fork Satin runs on is active atsatinTime.mega-evme --genesis FILEtakes a run's schedule from the chain's genesis file instead of the unknown-chain fallback. A run the file cannot configure is refused, chain first: another chain, a legacy spec, a Satin run beforesatinTime, a file without Satin keys.runandtxrun Satin when--specis left out.check_genesis_account, for a node loading its genesis. It refuses an account that a genesis allocation puts at a predeploy address with empty code, nonce 0 and storage (GenesisAllocationError): the deploy writes only the predeploy's seed slots and does not clear the old ones, so the committed state would keep them. A predeploy allocated with its own code, and a balance-only account, pass.SYSTEM_CONTRACT_ADDRESSESlists the addresses.no_user_tx_activation_blockand never works it out; a node sets it for every block from its schedule and the parent's timestamp, withMegaHardforks::admits_only_deposits.mega-evme replayandstate-test btestnow set it the same way.AGENTS.md, the crate README and themega-evmedocs.Randomized tests, and the engine fixes they found
crates/mega-evm/tests/fuzz) runs onproptestthrough its own runner: a ChaCha RNG seeded fromMEGA_FUZZ_SEED(default 0) and a fixed case count per property, so the bounded run every pull request makes is deterministic and takes seconds.MEGA_FUZZ_LONG=1runs fifty times the cases from a seed drawn from the clock and printed;MEGA_FUZZ_CASESnames an exact count. A failure panics with the seed, the case count and the case proptest shrank to within its shrink limits (two minutes, 4,096 steps), which is not always the minimal one.ProtocolLimits; blocks of up to six envelopes under random budgets and packing policy. A keyless deployment draws its own world (the signer, the deploy address, the carried transaction's encoding and signature, init code at the size limit, one byte past it or twice it), so each rule is reached. About one case in five is refused before it runs.KV × 40 ≤ data sizeafter every instruction, and every transaction keeps its body and forwarded hints; what a stop keeps; a keyless deployment's rules, answer and nonce; a block's counters and budgets; block determinism; the schedule's monotonicity in the byte prices. What does not hold by design is stated as what does: the reservoir gives way to the EIP-7623 floor and op-revm's failed-deposit rule; total gas is not monotone in the byte prices, soscripts/fuzz_price_monotonic.pycompares state and history gas along the same path across two processes.MegaEvmmatches op-revm byte for byte and revm's mainnet EVM for unpriced transactions. What onlyMegaETHhas is replaced by its plain counterpart; a deposit from an empty caller is left out of the op-revm arm, which the op-revm baseline pins. Ethereum on Amsterdam settles a contract destroyed in the transaction that created it by EIP-8246, which Satin's Osaka base does not have, so that one arm runs with every self-destruction taken out of init code; no case is skipped for it.a876ca88, the randomized tests' last commit before they merged with the rest of this branch.testand in release intest-release, as part ofcargo test --workspace.fuzz.ymlruns the long mode onworkflow_dispatch, in debug and in release, and the byte-price comparison at two prices; a schedule would fire only on the default branch.GasInspector(a test utility) records a byte no opcode is defined for asINVALID, keeping the byte, instead of panicking: an account delegating to itself runs its designator and halts on0xEF.The fixes
Each is pinned by a regression test and, where op-revm is the reference, by a case in the op-revm baseline.
runtime_oog_resultrebuilds the gas from the intrinsic cost, dropping that charge, and the settlement refilled it on top, into the reservoir, which the halt does not burn.first_frame_inputnow clears the charge when the base's first-frame input returnsNone.runtime_oog_unwind. A creation failing later, infirst_frame_input, already kept its bump.catch_errornow clears that latch and nothing else: the deposit keeps its body and every hint input it admitted.The Satin page states the rules the fixes rest on: the over-limit hint above; every admitted hint input stays counted through a revert, a halt, a later stop and a failed deposit, one that fails to decode included (a forwarded payload cannot be taken back, and admitting and decoding is work done); a failed deposit reports its whole gas limit as regular gas and no state or history gas on any ledger.
The stateless witness
get_accessed_bucket_idsbesideget_accessed_block_hashes. A node used to read the per-transaction cache, which held the last transaction's buckets alone. A failed lookup (an error, or a capacity below the minimum bucket) is not exported: it fails its transaction, which is in no block. The record covers one EVM's executions since the block started, dropped candidates included; a node running several EVMs for a block takes the union.MegaTransactionOutcome::oracle_reads), emptied before every transaction and system call. A node takes the reads of the transactions it includes from their outcomes, in block order; a recorder around the service would also see every candidate the builder dropped, and hand a validator the answers in the wrong order.test_utils::witness) records every read a block makes and replays it, on a strict database and environments that serve exactly a witness, twice: on the channel witness a node builds (WitnessKeys,WitnessRecord::from_channels: the keys the pre-block states and included transactions' returned states name, resolved against the pre-block database, the two exports, the oracle records), and on the record of every database read, which shows a block has no hidden input. A key a witness lacks is refused on the read and fails the comparison; the liveness checks are in the test plan.BLOCKHASH; delegations, creations,SELFDESTRUCT; deposits, among them one that empties the fee scalars, one that creates an absent L1 block contract and one that fails, whose oracle read is answered from its outcome; a database that cannot serve the L1 entry; reads gas or the volatile-access switch skips; the reads of a reverted or halted frame; failed and below-minimum SALT lookups.mega-state-testgainswitness::check_replayand a test over the fixtures, ignored by default and run with--ignoredandMEGA_STATE_TEST_FIXTURESset. Every Osaka and Amsterdam entry the runner can build replays in both modes: one the engine executes on both witnesses, one it rejects on the record of its database reads alone, since it has no returned state and is in no block.check_replaysays which (Replayed), the test counts the two apart, and the execution-spec gate's job runs it on every file.AGENTS.mdandREVIEW.md: the witness is a function of the block's states and exports alone; its values, code included, are the chain's; its keys are every account and slot the block loads, a failed frame's and a failed deposit's included; a validator either gets the included transactions' oracle records or runs no service and finds each answer in the Oracle's slot at its read, which reproduces the block only when every recorded answer is the slot's value there, so a node serving such validators gives them the records or leaves such a transaction out; the slot number is an input given beside the header.Tracer output
crates/mega-evm/tests/tracers/runsMegaEvmunder revm-inspectors'TracingInspectorand builds the geth views throughDebugInspector::get_result, the call a node makes, so the root frame'sgasis the transaction's gas limit; the EIP-3155 trace comes from the revm fork'sTracerEip3155. Each view is aninstasnapshot in the tree (tests/tracers/snapshots/), compared byte for byte; a change is reviewed withcargo insta review, one snapshot at a time.MegaLimitExceededkind and limit.Blockchain tests through the block executor
state-test btestimports everynetwork == "Osaka"test of the main release'sblockchain_teststhroughMegaBlockExecutor, built on aMegaEvmin the neutral configuration, with the loosest protocol limits a chain may carry (ProtocolLimits::loosest()), which no fixture transaction comes near under EIP-7825's gas cap. The state-test gate runs one transaction per test onMegaEvm; this one covers many transactions per block, many blocks per test, cumulative gas, receipts, the EIP-2935 and EIP-4788 pre-block calls, and blocks that must be refused.static/state_tests, the same tests filled for Osaka, which the state-test gate runs; theethereum/legacytestsrepository holds no Osaka entries.Tests held to the spec
Every clause of the Satin page was mapped to the test that owns it, the kind of evidence it uses, and whether its expected value is computed from the spec or measured from another run of the engine. The gaps that need no spec decision are closed here; each new or strengthened test was shown to fail on a planted engine defect.
MegaLimitExceeded(2, compute at the read + cap)); the same body as a Mega System Transaction is not.SELFDESTRUCTand an endowed creation, in the outcome and in a block's receipts.SSTORE,SELFDESTRUCT, the first frame, a nested value call and a creation.keylessDeploy'sNotIntercepted(), the pre-block witness's code, the pre-block calls' limits and block counters with the EIP contracts' code in state, the BN254 pairing bound, and a rejected transaction's sender read from the database its EVM ran on.The spec questions that needed a decision are now settled, and their tests added, with expected values computed by hand:
SELFDESTRUCT, is a write record; a write to the sender's account never is.Behaviour changes
Every change to what execution produces, or to what a block reads and when that read fails, with its rule.
first_frame_inputalready didgas_usedone record's history (3,520 at the spec's prices) short,reservoir_remainingthat much too high, history ledger the body less one recordgas_usedup andreservoir_remainingdown by 3,520: below the execution capgas_used = gas_limit,reservoir_remaining = 0; above it the transaction's own reservoir is returned; history ledger the body; regular ledger and history bytes unchanged3,520 × effective gas priceand a non-zero gas-dependent operator fee grows with the gas used, both paid by the sender to their recipients; the L1 data fee is unchanged; the receipt's cumulative gas, the block's gas used and the block's history ledger each grow by 3,520 per such transactionFailedDeposithalt and no stop; it keeps the data size it counted, its body and every hint input it admittedlimit_exceededreported the data-size stop beside the haltlimit_exceededisNone; gas, status, logs, state and usage unchanged (an outcome field, not a receipt field)usage.data_sizeand in the block's data-size counterused, its revert data, gas, status, logs and state unchangedPreBlockStateSource::L1BlockInfo); an absent account is recorded absent, with no slot readThe L1 read reaches tooling: a Satin replay of a block recorded on the legacy engine reads the overhead and the EIP-7997 factory, which the legacy engine never read.
mega-evme replay --blockfetches both over RPC on the first Satin run and caches them, and offline without them names what it lacks; any other offline cache of legacy recordings needs both added.The activation-block switch reaches tooling too:
mega-evme replayrefuses a non-deposit transaction in a Satin block in which a fork activates, andstate-test btestimports each block with the switch its schedule gives. No recorded block or fixture is such a block, so no existing replay or test result changes. The replay reads the parent's timestamp from the block cache; an older cache entry lacks it, and a Satin block whose answer depends on it then asks for one run with--rpc, which writes it back. A legacy block, or one the schedule cannot make an activation block, replays from the cache alone.Breaking
PreBlockStateSourcegainsL1BlockInfo, and the pre-block observer receives one more state per block, last: an exhaustivematchon the enum needs the arm.MegaTransactionOutcomegains the public fieldoracle_reads: a struct literal needs it.test_utils::OpcodeGasInfogainsbyte, andGasInspectorrecords an undefined opcode asINVALIDinstead of panicking.mega-evme's library:MainCmdgainsgenesis; the command line only adds--genesis.rust-version1.94.1 → 1.94 and its lock (MSRV above: the nineteen alloy 2.x crates,lru0.18.4 → 0.16.4, a newhashbrown0.16.1);mega-evmenablesalloy-primitives/serdeandserde/alloc, and addsproptestandalloy-trieas dev-dependencies.mutation.ymlloses the spec-gate jobs;build-and-test.ymlgainstest-release,system-contractsandmsrv.Release notes
CHANGELOG.mdis generated from merged pull-request titles, so the notes for this PR are here:spec_cfg, the configuration Satin runs on, for a node'sEvmEnvvalidate_transaction_stateless,IntrinsicGas,system::live_system_address)LimitStop,decode_mega_limit_exceeded)SatinChainConfig, provisional key format)SequencerRegistryConfigin a chain configuration's JSON--genesis)check_genesis_account,GenesisAllocationError,SYSTEM_CONTRACT_ADDRESSES)read_l1_block_info,L1_BLOCK_INFO_SLOTS,BucketRecord,OracleReadandOracleReadRecordwith their accessors,BucketMultipliers::cached, andmega-state-test'sMode::{context_with_envs, evm_with_envs}andwitness::{check_replay, Replayed}; the witness harness (test_utils::witness) is behindtest-utilscrates/mega-evm/tests/fuzz), bounded on every pull request and long on dispatchrust-version1.94, keepcargo updateon it, and check the workspace on it in CIOpen points for the team
gasUsedincludes the state and history gas its revert gives back. The call tracer's logindexcounts logs a revert discarded. The revm fork's EIP-3155 summary counts the reservoir left as used and leaves out a creation's deposit.mega-evme's trace builder does not set the root gas limit, unlike a node. None of these moves a result; each needs a decision on whether the node, the fork or revm-inspectors should change.satin*keys are provisional until a network publishes a genesis file carrying them. They are flat (satinTxDataSizeLimit), as the node's existing fork keys are, rather than a nested object; a node's genesis checker can then refuse a newsatin*key after the first snapshot in one rule.Documentation corrections
Statements in the spec page, the guides, the READMEs and this description checked against the code and corrected; two were made true in the code instead:
runreadme removed; the pending record's decision ids replaced by their reasons.Test plan
Run at the head of this branch on macOS (aarch64).
cargo fmt --all --check,cargo clippy --workspace --lib --examples --tests --benches --all-features --locked,cargo sort --check …, the riscvno_stdcheck,cargo metadata --locked, Prettier ondocs/**/*.mdand*.md,git diff --check: all clean.cargo tree -i revm: one revm, the fork.cargo test --workspace --locked: 1,783 passed, 0 failed, 4 ignored in debug (mega-evmlib 387, block 205, fuzz 20, satin 609, system 327, tracers 15;mega-evme141 and 2 ignored live-RPC tests;mega-state-test65, blockchain tests 12 among them, and the ignored fixture test; system contracts 6;state-test8); 1,768 in release (the gap is the debug-assertion tests).cargo insta test --workspace --check --unreferenced=rejectpasses with no unreferenced snapshot.cargo test -p mega-evm --features satin-price-override,test-utils: 1,566 passed, 0 failed, with the byte prices unset and atMEGA_SATIN_CPSB=2000 MEGA_SATIN_CPHB=100.scripts/price_grid.sh --pr: 0/0, 1/1, 312.5/20, 5000/300 and 10000/1000 each pass 1,566 tests with 0 failed;--fullpasses at all 73 points, fractional prices included; snapshot comparisons are skipped away from the spec's prices and counted apart.state-test bteston the main release'sblockchain_tests: 18,713 of 21,558 Osaka tests executed and 2,845 skipped by class as pinned; 20,232 blocks checked against Ethereum (19,638 matched, 594 refused as expected), 39 matched to the listed deviation; 0 unattributed, 0 unreproduced, gate passed.forge testincrates/system-contracts: 115 passed, 0 failed.msrvjob's two commands:cargo +1.94.0 check --workspace --all-targets --all-features --lockedand the riscvno_stdcheck, both clean.mega-evmsources) on the diff against the base branch's headc1c12431: 116 mutants, 79 caught, 37 unviable, 0 missed, 0 timed out, every shard's gate passed. The later commits change nomega-evmcode but a doc comment, so the mutant set is the same 116 and the gate on their own diff has nothing to mutate and passes; no suppression added.