Skip to content

feat(satin): node integration API, the stateless witness, randomized tests and four engine fixes - #400

Draft
RealiCZ wants to merge 282 commits into
cz/feat/satin-w6from
cz/feat/satin-w7
Draft

RealiCZ wants to merge 282 commits into
cz/feat/satin-w6from
cz/feat/satin-w7

Conversation

@RealiCZ

@RealiCZ RealiCZ commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

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).

  • Node integration. spec_cfg exports the configuration Satin runs on; validate_transaction_stateless validates a transaction for a pool without state and reports its intrinsic gas by ledger; LimitStop reads a limit stop off a result for an RPC error; SatinChainConfig is the one parser of a genesis file's Satin keys, and mega-evme --genesis runs a chain from its genesis file. The workspace declares and checks Rust 1.94.
  • The stateless witness. The witness is defined by what a block's execution returns and exports: the pre-block states, the included transactions' returned states, the block hashes and SALT buckets the block exports, and the oracle answers its transactions recorded. The engine gains the two exports and the pre-block L1 block info entry a node was missing; a harness replays blocks, and every Osaka and Amsterdam fixture the engine executes, from the witness a node would build; the execution-spec gate's job runs the fixture replay.
  • Randomized tests, a new fuzz test target of 20 tests on proptest: 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.
  • Tracer snapshots, a new tracers test target of 15 tests pinned with insta: 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.
  • Blockchain tests, a new state-test btest gate: every Osaka test of the execution-spec release's blockchain_tests imported block by block through MegaBlockExecutor in 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 snapshots, for outputs nothing else can judge. 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.
  • Tests held to the spec, rule by rule. A review of every clause of the Satin page against the test that owns it, and of how independent each expected value is, found gaps; this PR closes them, the ones that waited on a spec decision included once the spec stated the engine's behaviour, and makes a dozen tests that could not fail on their own mechanism able to.
  • The spec states what the engine does. Wherever the Satin page contradicted itself or the engine, or said nothing about a reachable path, it now states the engine's behaviour: a failed bucket or database read is a node fault and never a transaction outcome; an execution write to a fee account is a write record; the detention bill can pass its limit by what frames that halt on an opcode's static gas had left; failures before the first frame win over a stop the body latched; the calldata floor of a history-exempt transaction; keyless and Oracle edge cases; and the orders the tests had pinned. No code changes.
  • CI and coverage. Release tests, Foundry tests and an MSRV job on every pull request; the fixture witness replay in the execution-spec gate; receipt bytes, multi-block sequences and the spec page's numbers pinned; the empty spec-gate mutation jobs gone.
  • Five behaviour changes, listed with their rules below: four from the fixes, and the pre-block L1 read with its failure mode. No other execution result moves: both equivalence gates, the keyless differential record and the pricing table are as before.

CI and test coverage

  • build-and-test.yml gains test-release (cargo test --workspace --release --locked, which runs the randomized tests' bounded mode in release, where debug assertions are gone), system-contracts (forge test in crates/system-contracts) and msrv (below).
  • The debug test job runs cargo test --workspace through cargo insta test --check --unreferenced=reject, once: a snapshot that differs, a snapshot file no test compares, or a pending .snap.new in 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 in AGENTS.md.
  • mutation.yml loses 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.yml gains 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.yml also extracts the release's blockchain_tests and gates them with state-test btest on every pull request (the blockchain-tests section below).
  • exec-spec.yml says 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.
  • One mixed block pins every receipt's encoded bytes, logs bloom and cumulative gas, and the receipts root computed with alloy's trie helpers. Each receipt's cumulative gas is the running sum of the included transactions' MegaGasUsage::gas_used, and the header's gas used is the final sum.
  • A CREATE followed by SELFDESTRUCT in 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.
  • Three block sequences: a SequencerRegistry rotation taking effect across blocks, the EIP-2935 history buffer wrapping, and a user transaction in the block after an activation block.
  • Foundry tests cover the on-chain fallbacks of KeylessDeploy, MegaAccessControl and MegaLimitControl. No contract source and no generated bytecode changed.
  • tests/satin/spec_numbers.rs reads docs/spec/upgrades/satin.md and checks the prices, caps, limits and addresses it states against the constants and the schedule at the spec's own byte prices.

Node integration

  • MSRV 1.94. The workspace declares 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. The msrv job checks the workspace, every feature on, and the no_std target on rustc 1.94.0; both pass on 1.94.0 here. .cargo/config.toml sets the resolver's incompatible-rust-versions = "fallback", so a cargo update in 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 its EvmEnv through 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 own validate_env and validate_initial_tx_gas on an EVM over an empty database and returns IntrinsicGas { regular, state, history, floor }. MegaHandler::validate_env is 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 with system::live_system_address(db) from a Database or, through WrapDatabaseRef, a DatabaseRef.
  • What the least gas limit leaves out. The intrinsic state gas is zero under EIP-2780, and the history of a transaction's start records is charged when it runs, so 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.
  • A deposit is never rejected. op-revm includes a deposit that fails validation as a 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.
  • The live system address changes only in a block's pre-block step and holds for the whole block, so a pool reads it again for every block, from the state after that block's pre-block changes.
  • LimitStop, for RPC errors. MegaTransactionOutcome::limit_stop() and LimitStop::from_result(result, limit_exceeded) give the kind, limit and usage of the limit that stopped a transaction; decode_mega_limit_exceeded reads 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's limit_exceeded is captured when the transaction ends.
  • SequencerRegistryConfig in JSON, in ProtocolLimits' shape: camelCase, every field required, unknown fields refused.
  • SatinChainConfig, the one parser of a genesis config object's Satin keys: satinTime and one flat satin-prefixed key per registry seed and protocol limit (sixteen). It refuses a missing, unknown (any key starting with satin, in any case) or mistyped key, and a key given twice where the deserializer still shows both copies (mega-evme --genesis and a node parse into a serde_json::Value first, which keeps the last); it validates both params types and the schedule, serializes back to the same keys, and reads a number serde_json's arbitrary_precision represents as a map. It reads no other fork's key: a node checks at load that each fork Satin runs on is active at satinTime.
  • mega-evme --genesis FILE takes 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 before satinTime, a file without Satin keys. run and tx run Satin when --spec is 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_ADDRESSES lists the addresses.
  • The activation-block switch. Block execution reads no_user_tx_activation_block and never works it out; a node sets it for every block from its schedule and the parent's timestamp, with MegaHardforks::admits_only_deposits. mega-evme replay and state-test btest now set it the same way.
  • Docs: a "Node integrators" section on the Satin page, AGENTS.md, the crate README and the mega-evme docs.

Randomized tests, and the engine fixes they found

  • The target (crates/mega-evm/tests/fuzz) runs on proptest through its own runner: a ChaCha RNG seeded from MEGA_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=1 runs fifty times the cases from a seed drawn from the clock and printed; MEGA_FUZZ_CASES names 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.
  • Generators: programs of self-contained ops biased towards what the engine meters; six transaction shapes (call, creation, EIP-7702, deposit, system-address, keyless) at five gas tiers around the execution cap; worlds with crowded and failing SALT buckets, Oracle answers, a beneficiary that may be the sender, a delegation and tight valid 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.
  • Properties, one test each: no panic; the same outcome twice, under a recording inspector and on a reused EVM; the gas ledgers (spend, receipt figure, refund cap, the reservoir exactly, the history exemption, value conservation, the sender's nonce); KV × 40 ≤ data size after 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, so scripts/fuzz_price_monotonic.py compares state and history gas along the same path across two processes.
  • The neutral differential: under each fork's neutral configuration MegaEvm matches op-revm byte for byte and revm's mainnet EVM for unpriced transactions. What only MegaETH has 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.
  • Any seed, not seed 0: the pull-request jobs run seed 0, so a property true of seed 0 by luck passes them; a generator change redraws every case and may expose it, and a dispatched long run draws other seeds. The seed runs at this head are in the test plan; the same runs passed at a876ca88, the randomized tests' last commit before they merged with the rest of this branch.
  • CI: the bounded mode runs in debug in test and in release in test-release, as part of cargo test --workspace. fuzz.yml runs the long mode on workflow_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 as INVALID, keeping the byte, instead of panicking: an account delegating to itself runs its designator and halts on 0xEF.

The fixes

Each is pinned by a regression test and, where op-revm is the reference, by a case in the op-revm baseline.

  1. A transaction whose first frame could not start was handed back one record's history it no longer held. A history-paying call that carries value to another account, or a creation, pays its first frame's write record in pre-execution; when the runtime gas phase then cannot pay the frame's start (EIP-2780's charge for the account it adds, or a delegated recipient's code access), the fork's runtime_oog_result rebuilds 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_input now clears the charge when the base's first-frame input returns None.
  2. A creation transaction that ran out of gas in Satin's pre-execution kept its sender's nonce, so it could be included again and again. Its bail-outs (in particular, paying the history of the creation's start record) reverted the phase's checkpoint plainly, which took back the bump the creation frame makes; the three bail-outs now unwind through revm's runtime_oog_unwind. A creation failing later, in first_frame_input, already kept its bump.
  3. A failed deposit reported the stop its body had latched before op-revm refused it. catch_error now clears that latch and nothing else: the deposit keeps its body and every hint input it admitted.
  4. An Oracle hint that would cross the transaction's data-size limit was counted though never forwarded, so the block's data-size counter took a payload that never left the node. The limit is now checked before the bytes are counted; the protocol's own work, exempt from the limit, still counts and forwards every hint it admits.

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

  • The SALT bucket export. Every bucket the SALT environment answered with a valid capacity is recorded on the cache miss that asked, for the block; the executor empties the record when the block starts and exports it with get_accessed_bucket_ids beside get_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.
  • The L1 block info as a pre-block entry. A non-deposit transaction is priced against the L1 block contract, which op-revm and the executor read on the block's state, not through a journal, so the account and its slots landed in no state a node collects. The pre-block phase now reads the account and every slot the pricing can read (1, 3, 5, 7 and 8: the L1 base fee, the Ecotone fee scalars, the overhead, the blob base fee, the operator fee scalars) and hands them to the observer as read-only entries, last in the sequence; a chain without the contract has it recorded as not existing, with no slot read. Why the overhead is always in it is an open point below.
  • Per-transaction oracle records. The engine records every read a transaction makes through the oracle service, with the answer, on its outcome (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.
  • The harness (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.
  • The code in a witness is the chain's, not a returned state's: an EIP-7702 authority delegated anew is admitted against the delegation the chain held, and its returned state carries the one it wrote. A witness that took code from the returned states fails 9 Osaka and about 70 Amsterdam fixture entries.
  • The hard cases: every stop class; keyless rules that read state or refuse before any read; a system transaction after a registry rotation; oracle reads with and without the service, dropped candidates, a validator without a service, two answers for one slot; deploys, the factory, an activation block; 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.
  • The fixtures: mega-state-test gains witness::check_replay and a test over the fixtures, ignored by default and run with --ignored and MEGA_STATE_TEST_FIXTURES set. 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_replay says which (Replayed), the test counts the two apart, and the execution-spec gate's job runs it on every file.
  • The contract, in the Satin page's witness section, the glossary, AGENTS.md and REVIEW.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/ runs MegaEvm under revm-inspectors' TracingInspector and builds the geth views through DebugInspector::get_result, the call a node makes, so the root frame's gas is the transaction's gas limit; the EIP-3155 trace comes from the revm fork's TracerEip3155. Each view is an insta snapshot in the tree (tests/tracers/snapshots/), compared byte for byte; a change is reviewed with cargo insta review, one snapshot at a time.
  • Every scenario's regular, state and history gas is worked out from the schedule, the named constants and the byte prices, and held against the receipt, the tracer's root frame and the EIP-3155 summary. The prestate is held against what a recording database served the engine. Each relation was shown to fail on a planted wrong value.
  • The limit stops cover a transaction-level stop that rewrites every frame, a frame-budget crossing that reverts only its frame while the caller continues, and a detention stop at the compute of the read plus the cap, each with its MegaLimitExceeded kind and limit.
  • Where the tracer output a node serves differs from what the engine did, the tests name the shape, with an assertion a fix on either side fails (see Open points).

Blockchain tests through the block executor

  • state-test btest imports every network == "Osaka" test of the main release's blockchain_tests through MegaBlockExecutor, built on a MegaEvm in 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 on MegaEvm; 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.
  • Each accepted block's gas used, logs bloom, receipts root and state root are held to its header. The state root is computed after taking out exactly the accounts an OP chain adds and the fixture's pre-state lacks, each only in the exact shape it is added in: the seven predeploys deployed before every block, while each holds exactly what its deploy writes; OP's base-fee vault, while it holds exactly the base fees of the blocks accepted; and the L1-fee and operator-fee vaults, while they hold nothing. A block the fixture expects to be refused must be refused, and the chain stays at the previous block. The head is compared at the end, and the post-state when the chain ends on state its last header describes.
  • Skips are decided from a fixture's content before it runs, each class with its reason: withdrawals (62 tests) and blob transactions (2,540), which an OP chain does not have; EIP-7685 requests (174); header or body checks a node makes before execution (17); a transaction an OP block cannot encode, in a block expected to be invalid (2); and the state-test gate's storage collisions, which revm cannot see (50).
  • Deviations are listed block by block, with the outcome Satin produces (gas used, receipts root, state root and the bloom's hash), and the chain continues from Satin's own state after a listed block, so no later block goes unchecked. Three tests deviate under the existing rule that Satin's Osaka base carries four Amsterdam opcodes: 39 blocks. With those opcodes taken out, all of them match their headers.
  • The full set runs on every pull request in the execution-spec gate's job, from the archive it already downloads; it takes about 2 s locally. Any failure, an unread fixture, a listed test that does not deviate as listed, or a missing or malformed summary fails the job.
  • The legacy GeneralStateTests are covered through the release's static/state_tests, the same tests filled for Osaka, which the state-test gate runs; the ethereum/legacytests repository 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.

  • A user's deposit that reads volatile data is detained (MegaLimitExceeded(2, compute at the read + cap)); the same body as a Mega System Transaction is not.
  • EIP-7708 transfer logs keep execution order around a SELFDESTRUCT and an endowed creation, in the outcome and in a block's receipts.
  • History gas is unscaled by SALT at every charge site, and the pricing table's history column is held constant across multipliers.
  • A creation's deposited code, one gas short of its hash, its state gas and its history in turn, with every gas limit built from the spec's prices.
  • An unpayable state charge under an armed state-gas limit is an out-of-gas, not a stop, at SSTORE, SELFDESTRUCT, the first frame, a nested value call and a creation.
  • The state-gas limit tests take their limits from the schedule, not from a measured run; the history the tests expect is computed by hand, not through the engine's own pricing.
  • The reservoir's draw for a body-only transaction, and the check before the charge in a child frame, above the cap and at a frame start.
  • Tests that could not fail on what they claim now can: the history allowance, a nested keylessDeploy's NotIntercepted(), 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.
  • Tests that held only at whole-gas byte prices now hold, or are guarded with a note, at zero and fractional prices; the nightly full price grid passes at all 73 points.

The spec questions that needed a decision are now settled, and their tests added, with expected values computed by hand:

  • An execution write to any of the four fee accounts, by a value transfer or a SELFDESTRUCT, is a write record; a write to the sender's account never is.
  • The detention overshoot over frames that halt on an opcode's static gas: one frame, a chain of two that adds up, and a transaction that makes no charge after it and succeeds past the limit.
  • The calldata floor of a history-exempt transaction (a user deposit, a Mega System Transaction, a promoted deposit) by shape, below and above the execution cap.
  • A failure before the first frame wins over a stop the body latched: a deposit that fails validation, or cannot pay for its caller's account, is a failed deposit.
  • The order of the counts (transaction data size, transaction KV, frame data size, frame KV), and withheld gas returned as unspent and kept out of the gas that caps the refund.
  • Activation blocks built from the schedule as a node builds them, including Optimism forks from Jovian on; a failed bucket read at block level; a keyless deploy address left with a nonce and no code; a destroyed account's late balance burned without a log; a deploy over storage that neither writes nor clears it.

Behaviour changes

Every change to what execution produces, or to what a block reads and when that read fails, with its rule.

Transaction or block Rule Before After
A creation transaction that runs out of gas in Satin's pre-execution (in particular, on the history of its start's write record) A pre-execution bail-out unwinds as revm's runtime gas phase does: the checkpoint is reverted, and a creation's sender gets the nonce bump its frame would have made, so the included out-of-gas creation cannot be replayed; a later failure in first_frame_input already did the sender's nonce did not move the sender's nonce moves by one; this fix moves no gas, status or log
A history-paying value call to another account, or creation, whose runtime gas phase cannot pay its first frame's start (fix 1) Nothing is handed back for a first frame that never starts, as op-revm hands nothing back: its write record is never made gas_used one record's history (3,520 at the spec's prices) short, reservoir_remaining that much too high, history ledger the body less one record gas_used up and reservoir_remaining down by 3,520: below the execution cap gas_used = gas_limit, reservoir_remaining = 0; above it the transaction's own reservoir is returned; history ledger the body; regular ledger and history bytes unchanged
The same, at a gas price above zero as above the execution-gas fee was charged on 3,520 gas too few the execution-gas fee grows by 3,520 × effective gas price and 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 transaction
A deposit op-revm fails whose body had crossed the data-size limit A failed deposit reports the FailedDeposit halt and no stop; it keeps the data size it counted, its body and every hint input it admitted limit_exceeded reported the data-size stop beside the halt limit_exceeded is None; gas, status, logs, state and usage unchanged (an outcome field, not a receipt field)
An Oracle hint whose input would take a transaction held to the data-size limit past it It is neither forwarded nor counted (the protocol's own work, exempt from the limit, counts and forwards it); the limit stops the transaction, and the stop reports the usage the hint would have reached not forwarded, but its bytes stayed in the stopped transaction's usage.data_size and in the block's data-size counter not counted: the data size kept is the body, the records kept and the hints admitted before it; the stop's used, its revert data, gas, status, logs and state unchanged
Every block: the pre-block L1 read The last pre-block step reads the L1 block contract's account and, when it exists, slots 1, 3, 5, 7 and 8, and hands them to the observer as read-only entries (PreBlockStateSource::L1BlockInfo); an absent account is recorded absent, with no slot read read by the first non-deposit transaction's pricing, on the block's state, in no state a node collects; the overhead (5) only with empty scalars read pre-block, whatever the scalars hold; no receipt, state or gas moves, and the three recorded mainnet blocks' Satin replay records are byte-identical before and after
A block whose database cannot serve that read A database that cannot serve the account, or a slot of a contract the chain holds, fails the block before its first transaction, as an internal error (no verdict on the block), like every pre-block read the failure surfaced at the first non-deposit transaction, only for a slot its pricing read; a block of deposits alone never met it fails in the pre-block phase, even for a block of deposits alone or one that never reads the overhead

The 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 --block fetches 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 replay refuses a non-deposit transaction in a Satin block in which a fork activates, and state-test btest imports 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

  • PreBlockStateSource gains L1BlockInfo, and the pre-block observer receives one more state per block, last: an exhaustive match on the enum needs the arm.
  • MegaTransactionOutcome gains the public field oracle_reads: a struct literal needs it.
  • A block whose database cannot serve the L1 entry now fails in the pre-block phase (above).
  • test_utils::OpcodeGasInfo gains byte, and GasInspector records an undefined opcode as INVALID instead of panicking.
  • mega-evme's library: MainCmd gains genesis; the command line only adds --genesis.
  • Build: rust-version 1.94.1 → 1.94 and its lock (MSRV above: the nineteen alloy 2.x crates, lru 0.18.4 → 0.16.4, a new hashbrown 0.16.1); mega-evm enables alloy-primitives/serde and serde/alloc, and adds proptest and alloy-trie as dev-dependencies.
  • CI: mutation.yml loses the spec-gate jobs; build-and-test.yml gains test-release, system-contracts and msrv.

Release notes

CHANGELOG.md is generated from merged pull-request titles, so the notes for this PR are here:

  • Features
    • evm: export spec_cfg, the configuration Satin runs on, for a node's EvmEnv
    • evm: validate a transaction without state for a pool, reporting its intrinsic gas by ledger (validate_transaction_stateless, IntrinsicGas, system::live_system_address)
    • evm: record the oracle reads a transaction makes on its outcome
    • limit: read a limit stop off a transaction's result (LimitStop, decode_mega_limit_exceeded)
    • block: export every SALT bucket a block's execution answered, and hand the L1 block info to the pre-block observer
    • block: parse a chain configuration's Satin keys in one place (SatinChainConfig, provisional key format)
    • system: carry SequencerRegistryConfig in a chain configuration's JSON
    • evme: take a chain's Satin configuration from its genesis file (--genesis)
    • system: refuse a genesis allocation that gives a predeploy storage without code (check_genesis_account, GenesisAllocationError, SYSTEM_CONTRACT_ADDRESSES)
    • additive public items: read_l1_block_info, L1_BLOCK_INFO_SLOTS, BucketRecord, OracleRead and OracleReadRecord with their accessors, BucketMultipliers::cached, and mega-state-test's Mode::{context_with_envs, evm_with_envs} and witness::{check_replay, Replayed}; the witness harness (test_utils::witness) is behind test-utils
  • Fixes
    • evme, state-test: a replayed or imported block is told whether it admits only deposits, as a node tells it
    • evm: a transaction whose first frame cannot start is no longer handed its first write record's history back, as op-revm hands nothing back
    • evm: a creation transaction that runs out of gas in Satin's pre-execution bumps its sender's nonce, as revm's unwind of the runtime gas phase does, so it cannot be replayed
    • limit: a failed deposit reports the halt and not the stop its body latched before op-revm refused it
    • limit: an Oracle hint that would cross a transaction's data-size limit is not counted, so the block's data-size counter no longer takes a payload that never left the node
  • Tests
    • randomized tests of the Satin engine: properties on random programs, transactions, worlds and blocks, and the neutral differential against op-revm and revm's mainnet EVM (crates/mega-evm/tests/fuzz), bounded on every pull request and long on dispatch
    • a witness-replay harness over blocks and the execution-spec fixtures, the fixtures replayed in the execution-spec gate
  • Build
    • declare rust-version 1.94, keep cargo update on it, and check the workspace on it in CI

Open points for the team

  • The five-slot pre-block entry. Every block of a chain that holds the L1 block contract reads and carries the L1 fee overhead (slot 5), whose value decides no result on Karst; op-revm reads it when the scalars are empty at the block's first non-deposit transaction, which a deposit earlier in the block decides. It costs one slot per witness and one read per block, and makes a database that cannot serve the slot fail every such block. The alternative is to carry the read where it is made, which needs op-revm's fetch to report what it read: a change to the fork.
  • The over-limit hint is not counted. For a transaction held to the data-size limit, a hint whose input would cross it is neither forwarded nor counted, the stopped transaction keeps none of its bytes, and the block's data-size budget takes nothing, while the stop reports the usage the hint would have reached. Counting it, as the payload the node had materialised, is the other choice.
  • An admitted hint that fails to decode stays counted. It reaches no service, and it is kept because admitting and decoding an input is work the node has done either way. It is not the only counted byte that reaches no service: a decoded input's trailing bytes are counted and dropped, and with no oracle service installed every admitted hint is counted and goes nowhere.
  • Tracer output that differs from what the engine did. The opcode tracer shows no step of a keyless deployment: revm-inspectors enters a child frame only through a call opcode in its parent, and the keyless call frame runs none. A detention stop's last step reads as an out-of-gas costing the frame's gas, because the tracer sees the interpreter before the engine puts the gas back. A reverted frame's traced gasUsed includes the state and history gas its revert gives back. The call tracer's log index counts 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.
  • The genesis 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 new satin* 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:

  • test(state-test), ci(exec-spec): the fixture witness replay counts executed and rejected entries apart, and runs in the execution-spec gate.
  • docs: a genesis key given twice is refused only where the reader still sees both copies.
  • docs(spec), twice: limit defaults only where no chain configures them; who pays history gas and when the floor binds; the body's history and the execution cap; three Rex6 previous behaviors.
  • six docs commits: the guides, READMEs, review list and fuzz runner's docs brought up to the code; the stale run readme removed; the pending record's decision ids replaced by their reasons.
  • the replay step validates each run's summary; the guides say how the ignored fixture test runs and where the seventeen pressed-back entries come from.

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 riscv no_std check, cargo metadata --locked, Prettier on docs/**/*.md and *.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-evm lib 387, block 205, fuzz 20, satin 609, system 327, tracers 15; mega-evme 141 and 2 ignored live-RPC tests; mega-state-test 65, blockchain tests 12 among them, and the ignored fixture test; system contracts 6; state-test 8); 1,768 in release (the gap is the debug-assertion tests). cargo insta test --workspace --check --unreferenced=reject passes 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 at MEGA_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; --full passes at all 73 points, fractional prices included; snapshot comparisons are skipped away from the spec's prices and counted apart.
  • Execution-spec gate, equivalence mode: Osaka 19,465 executed, 52 skipped, 19,463 passed, 2 deviated as listed; Amsterdam 15,754 executed, 72 skipped, 15,717 passed, 37 deviated as listed; 0 unattributed, 0 unreproduced, gate passed on both.
  • The witness replay over every fixture file (executed and replayed on both witnesses / rejected and replayed on the record alone / skipped / failed): Osaka equivalence 18,738 / 777 / 2 / 0, Osaka Satin 16,853 / 2,662 / 2 / 0, Amsterdam equivalence 14,899 / 905 / 22 / 0, Amsterdam Satin 13,547 / 2,257 / 22 / 0; the CI step's own script, run locally, gives the same table.
  • state-test btest on the main release's blockchain_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.
  • The keyless differential record is byte-identical to the base's (2,431,045 bytes).
  • The randomized tests at this head: the bounded mode on seeds 0 to 59 in release and on 9001, 31337 and 777777 in debug, and 20,000 cases per property on seeds 401 to 404 in release, all pass.
  • forge test in crates/system-contracts: 115 passed, 0 failed.
  • On rustc 1.94.0, the msrv job's two commands: cargo +1.94.0 check --workspace --all-targets --all-features --locked and the riscv no_std check, both clean.
  • Mutation gate (it mutates mega-evm sources) on the diff against the base branch's head c1c12431: 116 mutants, 79 caught, 37 unviable, 0 missed, 0 timed out, every shard's gate passed. The later commits change no mega-evm code 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.
  • Liveness, each by a temporary edit of the engine, reverted: without the pre-block L1 read, 53 of the 59 witness cases fail; with a failed deposit's oracle reads dropped from its outcome, the failed-deposit oracle case fails.
  • Sweeps for internal reference codes, CJK text and internal-process words over the added lines, the commit messages and this description: none.

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.
…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.
…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
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.

This branch has not been deployed

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

Labels

api: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.

1 participant