feat(l1): support frames-devnet-0 (tests-frames-devnet@v0.1.0) - #7223
feat(l1): support frames-devnet-0 (tests-frames-devnet@v0.1.0)#7223ilitteri wants to merge 21 commits into
Conversation
|
Lines of code reportTotal lines added: Detailed view |
Benchmark Results ComparisonNo significant difference was registered for any benchmark run. Detailed ResultsBenchmark Results: BubbleSort
Benchmark Results: ERC20Approval
Benchmark Results: ERC20Mint
Benchmark Results: ERC20Transfer
Benchmark Results: Factorial
Benchmark Results: FactorialRecursive
Benchmark Results: Fibonacci
Benchmark Results: FibonacciRecursive
Benchmark Results: ManyHashes
Benchmark Results: MstoreBench
Benchmark Results: Push
Benchmark Results: SstoreBench_no_opt
|
Two halves: the execution rule ethrex was missing, and the harness needed to see it fail. EIP-8141 Rationale: "Cold/warm access costs for the frame's target account are charged within the frame's own gas_limit through the normal EVM warm/cold accounting, not through the per-frame cost." ethrex charged nothing and left the target cold, so a frame read its target for free and every later frame re-paid the cold price for an account an earlier frame had already touched -- gas the receipts trie and the header gasUsed both carry. The charge (and the EIP-7702 delegation access that follows the indicator) now comes out of the frame's own budget, and a frame that cannot afford to be entered forfeits its gas limit without executing. Three details the reference implementation pins, none of them obvious from the EIP text: - A VERIFY frame whose target has no code runs the protocol default code *instead of* an EVM. It builds no gas meter, so it neither pays the entry charge nor leaves its target warm. A SENDER or DEFAULT frame to a codeless account is not special: it runs an EVM over empty code and pays. - The target is warmed inside the frame's substate backup, so a failed frame contributes no warmth to later frames -- the shared journal absorbs a frame's accesses only when it succeeds. - A frame whose sender cannot fund its value never starts and spends nothing, where ethrex charged the full frame gas limit. Entering a frame reads the target account, so the EIP-7928 recorder is told about it too, before the frame runs and regardless of how it ends. Harness side, so `tests/amsterdam/eip8141_frame_transactions` from ethereum/execution-specs#3047 can run: a `Bogota` fork (Amsterdam + hegota_time, the pseudo-fork EELS fills these fixtures with), type-0x06 fixture deserialization including the `"0x"`-as-absent encoding of an implicit frame target and an ARBITRARY signature's signer, and the two TYPE_6_INVALID_{FRAME_FORMAT,SIGNATURE} exception names, whose structural rules ethrex enforces in the decoder. The two-pass parallel BAL check now runs for Amsterdam and later rather than Amsterdam exactly. 32 of the 36 frame-transaction fixtures pass; the 4 that remain are BAL mismatches on frames that revert or halt.
Cherry-picked from the hegota-devnet branch, where it was found by a devnet builder that stopped producing blocks. An EIP-8141 frame that reverts outside an atomic batch rolls back its state changes, its state gas and its logs, but left the EIP-7928 recorder holding the writes it had already recorded. The builder therefore produced a block whose access list disagreed with the state the block contains, and the block failed its own BAL validation on re-execution. Take a recorder checkpoint at frame entry and restore it whenever the frame fails, mirroring what the atomic-batch unroll already does. `restore` re-files a freshly-written slot as a read and leaves `touched_addresses` alone, so every access the frame made is still reported and only the reverted changes go. The write-then-read case is why the slot could vanish from the access list altogether rather than merely being over-reported: `record_storage_read` suppresses a read for a slot that is already written, so dropping the write without re-filing it as a read left the slot in neither storage_changes nor storage_reads. This is the last of the frame-transaction spec-test failures: all 36 fixture files from ethereum/execution-specs#3047 (415 filled cases) now pass. [ported to main: the regression test passes `None` for the stateless_validator parameter VM::new gained on main, the one-line adjustment frames-devnet-0 made in db75e42 when it merged main]
The frame entry charge used `+`, which levm's arithmetic_side_effects lint rejects; both operands are bounded by the frame's gas limit by construction, so `saturating_add` states that without changing behaviour. [ported to main from frames-devnet-0 commit 8ce49c9; that commit also fixed two identity_op sites in the eth/72 cell-request path, which exists only on the devnet branch and stays there]
…d target Found by the ethrex + Nethermind devnet, not by a fixture: the first frame transaction that paid a fresh address split the chain on the header `gasUsed`, ethrex reporting 22_910 against Nethermind's 119_766. EELS charges `charge_value_transfer_to_non_alive_account` inside `create_evm_from_frame`, so a frame whose value transfer revives an account that is not alive pays the NEW_ACCOUNT state cost at entry, out of its own gas limit and before it runs. ethrex charged nothing, so it billed the frame a bare cold access (3_000) where Nethermind billed the whole 100_000 gas limit -- the charge is larger than the limit that frame declared, so on their side the frame never ran at all. The frame's state-gas reservoir starts empty, so there is nothing to draw the charge from and it spills into the frame's execution gas in full. It also lands in the state dimension, after the per-frame baseline, so a frame that fails rolls it back and contributes none of it -- matching a frame that never created the account. No spec-test fixture covers this: every `tests/amsterdam/eip8141_frame_transactions` case that moves value moves it to an account the `pre` state already funds. All 14_900 blockchain fixtures still pass. The two existing tests that transferred to a code-less EOA were written before the charge existed and targeted an unseeded address, which is exactly the case that now costs more than their 50_000-gas frames could pay. They are about the default-code path, so their recipient is now seeded alive, and the revival case gets its own test covering both arms: unaffordable (frame forfeits its limit, account stays dead) and affordable (frame succeeds, billed cold access plus the state charge).
…arget Two consensus bugs, both from the same over-broad branch, both caught by the frame-transaction fixtures added to execution-specs#3047 after this branch was cut. EIP-8141 sends every frame except one through a top-level call: only a VERIFY frame whose resolved target has no code runs the protocol default code instead. ethrex took the default-code path for *any* codeless target, which is indistinguishable for a plain EOA -- empty code returns success having spent nothing -- but wrong for a precompile, whose dispatch happens inside that call. A SENDER or DEFAULT frame targeting one reported success with `gas_used` of zero and no output, where the precompile should have run and been billed. Fixed by branching on the frame's mode and the target's own code rather than on emptiness alone. The second is the order of the entry charges. The delegate of an EIP-7702 target was read before the frame was known to afford that access, so a frame whose gas could not cover it still filed the delegate in the EIP-7928 access list. The receipts cannot contradict that -- an unaffordable designation and a failure inside the delegate's code both forfeit the whole frame gas limit -- so the access list is the only place it shows. `eip7702_peek_delegation` already exists for exactly this ordering and is what the CALL-family handlers use; the frame path now uses it too, and resolves the delegate only once the charge is paid. Repins `.frames_spec_rev` to cd654d9a, which carries the target-resolution and block-access-list cases that caught both. All 44 frame fixtures pass, and 14908 blockchain fixtures overall. [ported to main: the .frames_spec_rev repin is dropped -- the local-fill pipeline never lands on main, since tests-frames-devnet@v0.0.0 ships these fixtures as a release and a later commit pins that instead. The docs rewrite of the frame-warmth section (originally from frames-devnet-0 commit 5b401e9) comes along here because it describes the entry-charge behaviour this commit and its predecessor implement; the old text documented the pre-charge world]
The Makefile fetches the Amsterdam bundle to `$(ARTIFACT).part` first so a failed download cannot truncate the previous one, but `*tests*.tar.gz` does not match the `.part` suffix, so a 698 MB partial was committable -- and was committed, then removed a commit later. GitHub rejects any pushed blob over 100 MB whether or not the tip still references it.
…uite
The `tests-frames-devnet@v0.0.0` release refills the whole suite at the
`Bogota` pseudo-fork, so it re-covers every Amsterdam EIP with frame
transactions active, not only the EIP-8141 tests. 3137 blockchain fixtures,
of which two failed.
EIP-8141 bounds a frame transaction's fee fields at 2**256, and one fixture
uses that room: `admission_constraints[max_cost_within_bound]` names a
`max_fee_per_gas` of `2**232 - 1` -- 29 bytes of `0xff` -- and expects the
transaction to be *valid*, since `max_cost` still fits. ethrex held both fee
fields in `u64`, so it could not decode the transaction at all and rejected
the block another client accepts.
The fields are now `U256`, and the two shared accessors widen with them; most
frame call sites were already wrapping them in `U256::from`, so they lose the
wrapper rather than gain one. `GenericTransaction` and the `feeHistory` reward
still narrow, and saturate rather than call `U256::as_u64`, which panics: both
report a fee a payer must be able to cover, so a value that large cannot
change either answer.
The other failure was the harness, and the three overflow exception names the
release introduces split two ways. `GASPRICE_OVERFLOW` and `PRIORITY_OVERFLOW`
name type-0x06 fixtures whose fee field carries 2**256 or more -- past the
bound the EIP puts on the field, and so past what the `U256` holding it can
represent. Those transactions cannot be decoded at all, so the two names map
to a decode-time rejection and the runner accepts that surface, the same shape
as the `NONCE_IS_MAX` case beside it. `GASLIMIT_PRICE_PRODUCT_OVERFLOW` needs
neither a mapping nor a tolerance: the name is already mapped for the other
transaction types, and its fixtures' values fit the fields that hold them -- a
legacy `gas_price` is `U256` as well -- so those blocks decode and are rejected
when the transaction is validated, exactly as the copies of the same fixture in
the other forks' subtrees already are.
Pipeline: `.fixtures_url_frames` pins the release and `make frames-vectors`
overlays it, keyed on the URL like the Amsterdam overlay, replacing the local
`uv run fill` of an unreleased revision. `make run-hive-eels-frames{,-rlp,-quick}`
run it under hive.
Hive needs one patch to be useful: its ethrex client maps
`HIVE_<FORK>_TIMESTAMP` onto genesis fields and stops at Amsterdam, so the
Bogota timestamp EEST sets reaches the client as nothing, frame transactions
stay pre-fork, and every EIP-8141 test fails while the rest of the Bogota
suite passes -- 24785 passed against 159 failed on the public dashboard, the
159 being exactly the EIP-8141 payload count. `patch-hive-frames-fork` adds
`bogotaTime` to the mapper in the clone, the same way run-hive-build-block
already patches `ethrex.sh`. The one-line fix belongs upstream in
ethereum/hive.
3137/3137 Bogota fixtures, 18058 blockchain fixtures overall on this base,
full workspace suite, clippy -D warnings and fmt clean in both workspaces.
[ported to main: the frames overlay fetches through .github/scripts/download.sh
like the Amsterdam overlay now does, instead of the raw curl-to-.part this
commit carried before main gained the retrying downloader; the RPC-receipt and
mempool replacement/eviction test hunks widened tests that exist only on
frames-devnet-0 and stay there with the mempool rules they cover]
Widening a frame transaction's fee fields to U256 widened the shared accessors with them, which broke the workspace under --features l2: the L2 fee-settlement integration test does its arithmetic in u64. Narrow at the read site, where the assumption that an L2 EIP-1559 transaction the test signs itself fits in u64 is local and checked, rather than widening the arithmetic.
grafana.com answers 403 to non-browser requests and leastauthority.com times out from CI runners; both serve fine in a browser. They failed the Link Check job on pages this branch does not touch (the monitoring docs and the audits index), so add them to the existing bot-blocked-hosts exclude list.
tests-frames-devnet@v0.1.0 fills against a revised EIP-8141 whose frame
encoding ethrex does not speak: a frame's single gas field becomes a nested
[execution, state] pair, the transaction's three fee fields become a nested
fees list moved after signatures, and a frame receipt's gas_used becomes an
{execution, state} pair. Measured on this branch, the v0.1.0 pin fails 72 of
18,089 blockchain tests -- exactly the files carrying a type-0x06 transaction
-- with InvalidBlock(InvalidBody(TransactionsRootNotMatch)), which is what a
re-encoding mismatch looks like in that harness.
The track therefore pins tests-frames-devnet@v0.0.0, the newest release this
implementation conforms to, and the doc now states what adopting the revision
needs: the encoding changes and the STORE_SCHEMA_VERSION bump the receipt
change forces, the intrinsic cost drop from 15,000 to 12,000 and the derived
gas anchors that move with the second dimension, the per-frame state-gas pools
with cross-frame refill attribution, the new SIGDATACOPY opcode and the
static-arity SIGPARAM it leaves behind, and the four semantic changes that
ship alongside them.
…flow Widening a frame transaction's fee fields to U256 left one addition unguarded. `calculate_gas_price_for_tx` computes `min(max_priority_fee + fee_per_gas, max_fee_per_gas)`, and with `max_priority_fee` now a U256 that sum can leave the type: EIP-8141's static constraints bound the fee fields only at 2**256, so a decodable frame transaction can carry a priority fee near U256::MAX. U256's `Add` panics on overflow rather than wrapping, and this runs during per-transaction env setup in block execution, so the panic would take the node down on a block a peer can hand it. Saturating is not an approximation here, it is the same answer: a sum past U256::MAX is above `max_fee_per_gas` by construction, so `min` clamps to the cap either way. This matches how `calc_effective_gas_price` keeps the same arithmetic in range by clamping before it adds. No behavior change off the frame path -- every other transaction type still holds u64 fees behind `U256::from`, so the sum stays below 2**65.
The header pinned `c55786f42`, but SIGPARAM's copy branch takes its operands in CALLDATACOPY's order, which is `4a9ad32cf` -- one revision later. Pin the revision the code actually implements, and name the two the code does not: `5276aa8cb` (ban SLOTNUM during validation-prefix execution) is a public-mempool admission rule and never reaches block validation, and `064f49621` is an editorial link pass.
The fee list did not move: at both revisions it sits between `signatures` and `blob_versioned_hashes`. What changed is that its three flat fields collapse into one nested list, which is what takes the payload from nine top-level items to seven. Say that, and give the item counts, so the note is enough to size the decoder work without re-deriving it from the fixtures.
`tests-frames-devnet@v0.1.0` fills against a revised EIP-8141 in which a frame declares a gas budget per gas dimension of EIP-8037 rather than one scalar. That reaches the wire format, so nothing about the revision is optional: ethrex could not decode a type-`0x06` transaction from the release at all. Encoding. A frame's fourth item becomes the nested list `[execution, state]`, and the transaction's three flat fee fields collapse into one nested `fees` list in place, between `signatures` and `blob_versioned_hashes` -- taking the payload from nine top-level items to seven. Both moves the transaction hash and the EIP-8141 signature hash, so `fees()` is the single definition the encoder and the signature hash share. A frame receipt's `gas_used` likewise becomes `[execution, state]`, and the RPC receipt reports the second dimension as `stateGasUsed`. Intrinsic cost. `TX_FRAME_INTRINSIC` drops from 15,000 to 12,000 -- equal to `TX_BASE`, because a frame transaction prices signature recovery per signature entry, so the recovery component of the base cost instead covers payer settlement. The intrinsic gains `TX_VALUE_COST` (6,000) per value-bearing frame whose explicit target is not the sender, covering the recipient balance write and the transfer log. Both anchor the calldata floor as well as the execution cost. Derived anchors. `standard_gas_limit` spans both dimensions; `max_gas` compares it against the calldata floor plus the state budgets, because the floor binds the execution dimension alone -- a floor-bound transaction pays the floor plus its state gas in full. EIP-7825's per-transaction cap bounds the execution dimension alone, and block capacity is reserved exactly per dimension rather than through the reservoir model, which exists for transaction types that cannot name a state budget of their own. Metering. Each frame runs against a state-gas pool seeded from its declared budget. The dimensions are independent, so a charge past the pool is an out-of-gas halt rather than a spill, and leftover state gas does not fund execution. A frame transaction therefore carries no reservoir. Because a storage refill may reverse a charge an *earlier* frame paid, the owner of each outstanding charge is tracked and the refill lowers the owner's receipt in place -- returning the gas to the payer at settlement without granting the executing frame budget it never declared. Frame entry, and atomic-batch entry, snapshot the receipts, the approval fields, the pool and the ownership map; a batch unroll re-appends the executed frames' receipts with their state gas zeroed and logs dropped, since the state those charges paid for is exactly what the unroll discards. Semantics the revision changes: the resolved target's warm or cold access is charged at entry for every frame, codeless `VERIFY` included, and priced before the target is warmed -- so a frame that cannot afford its entry never reads its target and nothing about it reaches the block access list. A revival's `NEW_ACCOUNT` cost is charged to the state dimension. A frame targeting an active precompile dispatches it in every mode. An `APPROVE` refusal reverts the current call frame instead of halting, with the memory expansion charged first, following `RETURN` semantics for the designated return-data region. Approval scope is statically rejected on every frame of an atomic batch, and an expiry verifier frame must declare no state gas. The validation-prefix budget splits with the dimensions: the prefix's execution budgets plus the signature cost against `MAX_VERIFY_GAS`, its state budgets against the separate, much larger `MAX_VERIFY_STATE_GAS`. State gas does not measure a node's simulation work, so it must not consume the budget that bounds it. Nothing here is reachable off the frame path. The per-frame pool is `None` for every other transaction type, so the two shared EIP-8037 helpers fall through to the existing reservoir logic unchanged, and `SIGDATACOPY` joins the opcode table only where the other frame opcodes do -- the table test now pins 0xB5 invalid at Osaka and Amsterdam alongside them. Also corrects three comments that gave the skipped-frame receipt status as 3. The constant has been 2, and the reference enum at the release tag confirms it.
The release supersedes `v0.0.0` and fills every fork through Bogota against the revised EIP-8141. `for_bogota` grows from 3,137 fixture files to 3,168, and the EIP-8141 subtree from 45 files to 71 -- new coverage for the state-gas dimension, `SIGDATACOPY`, the frame-entry access charge and the signature-validation precompiles staying out of the block access list. `make -C tooling/ef_tests/blockchain test-levm` passes 18,089 of 18,089: the 3,168 Bogota files and the 14,921 that were already green. Repins the doc's spec target to `7d1c8bfb9`, the EIP-8141 head when the release was cut, and rewrites the sections the revision falsified -- the wire encoding, the gas anchors, the per-frame metering, the frame-entry charge, and the `FRAMEPARAM` and `SIGPARAM` tables. Two limitations replace the note that said the revision was unimplemented: The validation-prefix banned-opcode list has not been re-synced. ethrex still bans the four opcodes the revision relaxed, which can only over-reject, but does not ban `SLOTNUM`, which is the direction a local peer policy should not err in. The rules govern mempool admission rather than block validity, so no blockchain fixture exercises the difference. `STORE_SCHEMA_VERSION` stays at 3 even though a persisted frame receipt changed shape. The affected records exist only on a chain that activates the pseudo-fork, which no shipped network configuration does apart from the local devnet fixtures; the same revision changes the transaction encoding, so a database holding frame transactions is undecodable at the block level too and a migration could not rescue it; and the frame-receipt storage layout was introduced the same way, in #6326, without a bump.
`golden_frame_tx_rlp_and_sig_hash` locks the type-`0x06` payload at the byte level, and the two-dimensional gas model moved every byte of it: each frame's fourth item is now the nested list `[execution, state]` and the three fee fields are one nested `fees` list, so the payload carries seven top-level items instead of nine. Repinned to the revised shape, along with the signature hash the same encoding feeds. Both expected values were derived from the reference's `FrameTransaction` and `Frame` field order rather than read back out of the encoder: the payload hand-encodes to 176 bytes (`f8b0`), and the signature hash is keccak256 over `0x06` and the same payload with the empty-msg signature's bytes elided, which is what `compute_frame_signature_hash` specifies. The comment above them claimed the values were "the current canonical output", which is circular, and that no external reference vectors existed, which the release ends. It now says where the bytes come from and why a byte vector is the right assertion: this crate's encoder and decoder would agree with each other on a flattened payload, so the round-trip below cannot catch a dropped nesting level -- only the vector can. `frame_receipt_gas_used_encodes_as_a_nested_pair` is new, for the same reason on the receipt side. Every other test in `receipt.rs` builds its expectation with the same encoder it exercises, so none of them would notice `gas_used` flattening back to a scalar; outside this crate only the receipts root would, through the spec-test fixtures.
Ten of the in-repo EIP-8141 tests were asserting the gas model the two-dimensional revision replaced. Six of them declared `state: 0` on frames that create a storage slot: the dimensions are independent now, so a state charge draws on the frame's own state budget and cannot spill into its execution gas, and those SSTOREs run out of state gas. Two of the six still carried a comment saying the state cost "spills into the frame's regular gas", which is exactly the replaced model. The frames that create a slot now declare `SSTORE_SET_STATE_GAS`, the cost the file already computes for that transition. Two further tests are changed although they were *passing*, because they were passing vacuously and would have kept passing untouched: `sigparam_0x01_returns_the_scheme` asserts the stored value is `FRAME_SIG_SCHEME_ARBITRARY`, which is 0, so its SSTORE was writing 0 over 0 and the assertion held whether or not the frame executed; and `reverted_frame_reports_no_state_gas` could not tell a REVERT that discards state from an SSTORE that never ran. `state_gas_reservoir_does_not_leak_across_frames` becomes `state_gas_does_not_leak_across_frames`: there is no reservoir on the frame path any more. What it asserts is unchanged and is what the per-frame budget enforces. One of the ten failed on static validity rather than gas. `timestamp_inside_expiry_verifier_is_allowed` builds its frame with the shared observer helper, which budgets both dimensions, and an expiry verifier frame runs protocol code that creates no state, so the revision rejects one declaring any state gas. That frame is now built inline with no state budget; the helper is left alone, since every other observer test wants both. The remaining three covered SIGPARAM's copy selector `0x04`, which is gone -- the copy is `SIGDATACOPY`'s job, which is what makes SIGPARAM static-arity. They move to the opcode that performs the copy, keeping their intents (copies bytes, zero-fills past the end, operands in `CALLDATACOPY` order). A new test pins the removal itself: a handler that still accepted `0x04` would read the wrong operand count off the stack and nothing else here would notice.
The header's spec-tests note pointed at an anchor that no longer exists: the banned-opcode limitation was reworded when the two-dimensional gas model landed, and the link kept the old heading's slug. The file-reference tables and the out-of-frame halt list also predate `SIGDATACOPY`, so they named every frame opcode except the new one. Its gas entry records that it shares `framedatacopy()`; the reference gives the two opcodes the same base and the same per-word cost, so there is no separate constant to cite.
The spec-target header named EIP-8141 @ `7d1c8bfb9`, derived from the EIP's commit history as the head when `tests-frames-devnet@v0.1.0` was cut. The fixtures say otherwise, and they say it outright: all 214 EIP-8141 cases in the pinned release carry `_info.reference-spec-version = 3ceef8d37`, the commit immediately before `7d1c8bfb9`, with no variance across the 71 files. Quote the fixtures rather than the commit graph. Being one revision out also made the header's completeness claim false. It said a single rule of the target revision was unimplemented -- the validation-prefix banned-opcode list -- when `7d1c8bfb9`'s own normative change is a second. That commit adds EIP-7778 to the EIP's `requires` list and counts the execution dimension a frame transaction contributes to the block *before* its EIP-3529 storage refund, while the payer keeps paying the post-refund figure. ethrex puts one post-refund total in both `ExecutionReport::gas_used`, which the block counters read, and `gas_spent`, which receipts read. So the header now lists two unimplemented rules and the new one gets a Known Limitations section. Record the rule rather than implement it, because the pinned release requires the older one and fixes it in a header: `gas_settlement/storage_refund_settlement.json` clears a pre-existing storage slot and pins the reduced counter at `gasUsed = 0xf4ec`. Accounting the frame path pre-refund fails exactly that fixture, `GasUsedMismatch(74316, 62700)`, the 11,616 delta being one uncapped single-slot clear refund, and changes no other EIP-8141 fixture. The release's reference implementation carries the same asymmetry deliberately: at the release tag `settle_frame_transaction_gas` describes the execution dimension as the post-refund usage less the attributed state gas, while the regular-transaction settlement beside it cites EIP-7778 for counting pre-refund gas. The change belongs with the release that fills against `7d1c8bfb9`, and it needs the frame-tx report to separate its block-facing figure from its payer-facing one, the way `refund_sender` already does on the regular path.
`From<Transaction> for FrameTransaction` documents itself as staying total, so that a fixture carrying a deliberately out-of-range field reaches the validity check that rejects it for that field rather than aborting the harness on the way there. Three fields did not follow the rule: a frame's `mode` and `flags` and a signature's `scheme` are `U256` on the fixture side and `u8` in ethrex, and converted with `.unwrap()`, so any value past 255 panics. Saturate them like the other scalars rather than narrow the comment to excuse them, because the comment's own safety argument covers them unchanged: the runner rebuilds the block from these fields and `validate_block_body` recomputes the transactions root against the header the fixture ships, so a field altered by saturation fails the root check loudly instead of passing quietly. The fixtures this guards are already on the path and already at the boundary. A block whose transaction ships as raw RLP is taken from the fixture's `rlp_decoded` view, which this conversion consumes, and the invalid-frame-format fixtures there declare `mode`, `flags` and `scheme` up to exactly `0xff` -- the last value that fits. Nothing in the release exceeds it, so the suite is unchanged at 18,089.
…spelled out The two-dimensional gas model changed the consensus frame receipt from `[status, gas_used, logs]` to `[status, [execution_gas, state_gas], logs]`, but three places that expand that layout inline kept the scalar: the Receipt Format section of `docs/eip-8141.md`, which is the definition the rest of the section's `frame_receipt` token refers back to; its restatement in the argument for deriving `succeeded` from every frame's status; and the doc comment on `receipts70_frame_receipt_matches_the_eip8141_wire_form`. Every other site names the token instead of expanding it, so it needed no change. The section's one-line gloss on the gas -- "the frame's gas, not accounting for refunds" -- was wrong in a second way once the field became a pair, because the two dimensions do not treat refunds alike. The reference at the release tag documents the execution dimension as "not reduced by refunds. Final once the frame completes" and the state dimension as "attributed to the frame after all refills and rollbacks applied so far", which is exactly what lets a later frame's cross-frame refill lower an earlier frame's entry in place. Say that instead.
144a138 to
18bb089
Compare
Motivation
First main-branch test track for the frames stream. Until now the EIP-8141 spec-test work lived only on the
frames-devnet-0dev branch; main carries the frame-transaction implementation (#6326, #7004, #7068, #7073, #7089) but nothing runs the upstream fixtures against it. This PR pins the frames-devnet bundle, adds the overlay and harness wiring that runs it, and brings the implementation up to the EIP-8141 revision that release fills against.Description
The bundle refills the whole suite at the
Bogotapseudo-fork (Amsterdam + EIP-8141, activated bybogotaTime— ethrex'sFork::Hegota), so the newfor_bogota/subtree re-covers every other Amsterdam EIP with frame transactions active, not just the 8141 tests: 3,168 fixture files carrying 25,017 blockchain test cases (counted as top-level case keys), every one atnetwork: Bogota, of which 71 files are the EIP-8141 subtree.Most of this PR is the EIP-8141 revision itself.
tests-frames-devnet@v0.1.0fills against a spec in which a frame declares a gas budget per gas dimension of EIP-8037 rather than one scalar, and that reaches the wire format — so none of it is optional. Before this change ethrex could not decode a type-0x06transaction from the release at all: every one of the 71 EIP-8141 fixture files failed, along with the one non-8141 file whose cases parametrise over the fork's transaction types. No branch implemented the revision,frames-devnet-0included, so it is derived here from the reference implementation at the release tag, the EIP text at the revision the fixtures themselves name in_info.reference-spec-version(3ceef8d37, uniform across all 214 EIP-8141 cases), and the shipped fixtures.One rule of the next EIP revision is deliberately not adopted.
7d1c8bfb9, three days after the one the fixtures name, counts a frame transaction's block execution gas before its EIP-3529 storage refund, per EIP-7778, while the payer keeps paying the post-refund figure. The pinned release requires the older rule and fixes it in a header —gas_settlement/storage_refund_settlement.jsonclears a storage slot and pins the reduced counter — so adopting the newer one here would turn that fixture red, and the release's own reference implementation documents the post-refund form too. It is recorded under Known Limitations indocs/eip-8141.md, with what adopting it will take: the frame-tx execution report separating its block-facing figure from its payer-facing one, which the regular transaction path already does.Encoding. A frame's fourth item becomes the nested list
[execution, state], and the transaction's three flat fee fields collapse into one nestedfeeslist — in place, at the position they already occupied betweensignaturesandblob_versioned_hashes, taking the payload from nine top-level items to seven. Both move the transaction hash and the EIP-8141 signature hash, so the nesting lives in one place that the encoder and the signature hash share rather than being spelled twice. A frame receipt'sgas_usedbecomes[execution, state]too, and the RPC receipt reports the second dimension asstateGasUsed.Intrinsic cost.
TX_FRAME_INTRINSICdrops from 15,000 to 12,000 — equal toTX_BASE, because a frame transaction prices signature recovery per signature entry, so the recovery component of the base cost instead covers payer settlement, which regular transactions do not price separately. The intrinsic gainsTX_VALUE_COST(6,000) per value-bearing frame whose explicit target is not the sender, covering the recipient balance write and the transfer log; a frame paying its own sender moves no funds between accounts and is not charged. Both anchor the calldata floor as well as the execution cost.Derived anchors.
standard_gas_limitspans both dimensions, andmax_gascompares it against the calldata floor plus the state budgets, because the floor binds the execution dimension alone — a floor-bound frame transaction pays the floor plus its state gas in full, unlike the single-dimension flow where the floor caps the whole transaction. EIP-7825's per-transaction cap likewise bounds the execution dimension alone. Block capacity is reserved exactly per dimension: a frame transaction names its state budgets explicitly, so it reservesmax(intrinsic + Σ execution, calldata_floor)against the block's execution dimension andΣ stateagainst its state dimension, rather than charging its whole limit against both as the EIP-8037 reservoir model does for transaction types that cannot name a state budget of their own.Metering. Each frame runs against a state-gas pool seeded from its declared budget. The dimensions are independent, so a charge past the pool is an out-of-gas halt rather than a spill into execution gas, and leftover state gas does not fund execution; a frame transaction therefore carries no reservoir at all. Because an EIP-8037 storage refill may reverse a charge an earlier frame paid, the owner of each outstanding charge is tracked and a cross-frame refill lowers the owner's receipt in place — returning the gas to the payer at settlement without granting the executing frame budget it never declared. Frame entry, and atomic-batch entry, snapshot the receipts, the approval fields, the pool and the ownership map; a batch unroll re-appends the executed frames' receipts with their state gas zeroed and logs dropped, since the state those charges paid for is exactly what the unroll discards.
Semantics the revision changes. The resolved target's warm or cold access is charged at frame entry for every frame, codeless
VERIFYincluded, and priced before the target is warmed — so a frame that cannot afford its entry never reads its target, and nothing about that target reaches the block access list. A revival's EIP-8037NEW_ACCOUNTcost is charged to the state dimension. A frame targeting an active precompile dispatches it in every mode,VERIFYincluded. AnAPPROVErefusal reverts the current call frame instead of halting exceptionally, with the memory expansion charged before the guards, followingRETURNsemantics for the designated return-data region. Approval scope is statically rejected on every frame of an atomic batch, and an expiry verifier frame must declare no state gas.Opcodes.
SIGDATACOPY(0xB5) takes over the signature-bytes copy thatSIGPARAMselector0x04used to perform, which makesSIGPARAMstatic-arity over selectors0x00–0x03— and0x03now halts for a protocol-validated entry, whose raw bytes are not introspectable and so have no length to report.FRAMEPARAMgains0x09(state gas limit),0x0Aand0x0B(the frame's receipt usage per dimension), andTXPARAMgains0x0C(the executing frame's remaining state gas).The validation-prefix budget splits with the dimensions: the prefix's execution budgets plus the signature cost against
MAX_VERIFY_GAS(100,000), its state budgets against the separate and much largerMAX_VERIFY_STATE_GAS(500,000). State gas does not measure a node's simulation work, so it must not consume the budget that bounds it.The in-repo tests move with the model: ten of the EIP-8141 unit tests were asserting the one it replaced. Six declared no state budget on frames that create a storage slot, which the revision turns into an out-of-state-gas halt rather than a spill into execution gas. Three covered
SIGPARAMselector0x04. One failed on static validity rather than gas — an expiry verifier frame runs protocol code that creates no state, so the revision rejects one that declares any state budget. Two further tests were fixed although they were passing, vacuously: one asserts a stored value of zero, so its SSTORE was writing zero over zero and the assertion held whether or not the frame ran, and the other could not tell aREVERTthat discards state from an SSTORE that never happened. The three selector tests move toSIGDATACOPY, keeping their intents, and a new one pins the removal, since a handler that still accepted0x04would read the wrong operand count off the stack and nothing else would notice.golden_frame_tx_rlp_and_sig_hashis repinned to the seven-item payload — its expected bytes derived from the reference's field order rather than read back out of the encoder — and the receipt's nestedgas_usedgets a byte vector of its own. Both are byte vectors rather than round-trips for the same reason: an encoder and decoder that dropped a nesting level together would still agree with each other, so only a fixed expectation catches it.None of this is reachable off the frame path, so no pre-Hegota behavior changes. The per-frame state-gas pool is
Nonefor every other transaction type, and the two shared EIP-8037 helpers enter their new branch only when it isSome, falling through to the existing reservoir logic otherwise; the only place the pool is ever populated is insideexecute_frame_tx.SIGDATACOPYjoins the opcode table only where the other frame opcodes do, and the existing "frame opcode must be invalid at Osaka/Amsterdam" test now pins 0xB5 alongside them.Track wiring, following the Amsterdam overlay's shape:
tooling/ef_tests/.fixtures_url_framespins the release (fixtures_frames-devnet.tar.gz).make frames-vectorsoverlaysfor_bogota/ontovectors/eest, keyed on the release URL like the Amsterdam overlay, fetching through the retryingdownload.sh.download-test-vectorsandtest-levmdepend on it, so the existing "Run Blockchain EF tests" CI job covers the track with no workflow change — the same way the Amsterdam bundle is covered.Bogotafork config (Amsterdam +hegota_time), type-0x06 fixture deserialization (including the"0x"-as-absent encoding of an implicit frame target and an ARBITRARY signature's signer), and mappings for the exception names the release introduces:TYPE_6_INVALID_{FRAME_FORMAT,SIGNATURE}, plusGASPRICE_OVERFLOW/PRIORITY_OVERFLOW, whose fixtures surface at RLP decoding because their 33-byte type-0x06 fees are 2**256 or more — beyond the bound EIP-8141 puts on fees — and so do not fit theU256fields. (GASLIMIT_PRICE_PRODUCT_OVERFLOWneeded no new mapping and no decode tolerance: its name→exception mapping pre-exists on main, and its fixtures' fee values fitU256— legacygas_priceincluded — so they decode fine and are rejected at execution, exactly as the same fixtures already pass in main's other fork subtrees.) The two-pass parallel BAL check now runs for Amsterdam and later rather than Amsterdam exactly. The fixture-to-ethrex frame conversion saturates every scalar it narrows instead of panicking, so a fixture carrying an out-of-rangemode,flags,scheme, chain id, nonce or frame gas limit reaches the validity check that rejects it for that field rather than aborting the run on the way there; a saturated value cannot pass silently, because the block is rebuilt from these fields and its transactions root is checked against the header the fixture ships.make run-hive-eels-frames{,-rlp,-quick}pluspatch-hive-frames-fork, which addsbogotaTimeto hive's ethrex genesis mapper in the clone (the mapper stops at Amsterdam upstream; without it every EIP-8141 payload runs pre-fork and fails).Scope note: the release fills every fork through Bogota, so the bundle carries far more than this track runs — its blockchain format alone holds 122,824 cases across 25
for_<fork>subtrees, and it also shipsblockchain_tests_engine/,blockchain_tests_engine_x/,state_tests/,transaction_tests/andblockchain_tests_sync/. Onlyfor_bogota/(25,017 cases) is overlaid. The other 24 cover forks the mainlinetests@v20.0.1and Amsterdamtests-glamsterdam-devnet@v8.1.1pins already supply — every one of those 24 subtree names is already populated by another pin — and all three pins extract into the singlevectors/eest/root, so taking them from this bundle would override those pins rather than add coverage. The non-blockchain formats each need a Bogota mapping in their own harness and can follow separately; hive'sframes/frames-quickworkflows are the release's designated engine-format vehicle in the meantime.Ported from
frames-devnet-0, keeping the dev branch's reasoning — the implementation the released fixtures exercise that main did not yet carry:666703940): EIP-8141 Rationale — the target's cold/warm access cost is charged inside the frame's own budget, plus the EIP-7702 delegation access behind it. Warmth lands in the frame's substate backup so a failed frame leaves nothing warm.b24600017): a reverting non-batch frame restores a recorder checkpoint, mirroring the atomic-batch unroll, so the block access list agrees with the state the block carries (write-then-read re-files as a read).8817d7911): matches the reference'scharge_value_transfer_to_non_alive_accountat frame entry, found as agasUsedchain split against another client.d39f4f547): only a codeless VERIFY target takes the protocol default code; a SENDER/DEFAULT frame to a precompile must run it.U256(dev999bb95dd,c2161f3f2): EIP-8141 bounds the fee fields at 2**256 and the release uses the room on a transaction it expects to be valid; ethrex'su64fields could not decode it. The two shared accessors widen with the fields;GenericTransactionand thefeeHistoryreward still narrow, saturating rather than panicking. One addition needed guarding rather than widening:calculate_gas_price_for_txcomputesmin(max_priority_fee + fee_per_gas, max_fee_per_gas), and sinceU256'sAddpanics on overflow instead of wrapping, a frame transaction carrying a priority fee near2**256— decodable, because the EIP bounds the field only there — would panic during block execution. It saturates now, which is the same value rather than an approximation: a sum pastU256::MAXis abovemax_fee_per_gasby construction, so the enclosingminclamps to the cap either way.8ce49c97b) and.gitignorefor*.tar.gz.partpartial downloads (dev1d8a5ed58).Deliberately left on the dev branch, because no fixture this track runs reaches it:
toreporting.frames-devnet-0.yamlkurtosis config, which depends on the dev-only glamsterdam-devnet-8 base config.docs/eip-8141.mdis repinned to the revision the code now implements and rewritten where the revision falsified it — the transaction and receipt wire encodings, the gas anchors, the per-frame metering, the frame-entry charge, the atomic-batch unroll note, and theFRAMEPARAM/SIGPARAMtables. Two known limitations replace the note that said the revision was unimplemented. The first is that the validation-prefix banned-opcode list has not been re-synced: ethrex still bans the four opcodes the revision relaxed (ORIGIN,BLOBHASH,TLOAD,TSTORE), which can only over-reject, but it does not banSLOTNUM, which is the direction a local peer policy should not err in. The second is the schema version, below.STORE_SCHEMA_VERSIONstays at 3, and that was checked rather than assumed. A persisted frame receipt genuinely does change shape here —Receipt::encode_storagecarriesframe_receipts, andgas_usedgoes from a scalar to a nested pair — so the question is real. Three things decide it. The affected records exist only on a chain that activates the pseudo-fork, and the only shipped configurations that do are the local devnet fixtures, so no mainnet or public-testnet database can hold one. The same revision changes the type-0x06transaction encoding, which means a database holding frame transactions is undecodable at the block level as well: a schema migration cannot rescue it, and bumping the version would advertise an upgrade path that does not exist, while the framework's compile-time invariant would demand a migration function for unsalvageable data. And the frame-receipt storage layout was introduced the same way in the first place, in #6326, which added it without a bump. The devnet restarts from a new genesis with each spec revision, which is the actual remedy.crates/storage/is untouched by this PR.CI housekeeping picked up along the way:
grafana.comandleastauthority.comjoinedlychee.toml's bot-blocked-hosts exclude list. Both serve fine in a browser but 403/time out from CI runners, and they were failing the docs Link Check on pages this PR does not touch (the monitoring docs and the audits index).How to test
make frames-vectorsfetches and overlays the bundle on its own; it is a prerequisite oftest-levm, and it is keyed on the release URL, so re-running it after a pin change re-fetches and re-extracts rather than reusing whateverfor_bogota/is on disk.Results from this branch:
make -C tooling/ef_tests/blockchain test-levm: 18,089 passed / 0 failed / 0 ignored — 14,921 pre-existing plus the 3,168 newfor_bogotafiles.make -C tooling/ef_tests/blockchain test-stateless: 3,218 passed / 0 failed.make -C tooling/ef_tests/engine test: 11,040 fixtures passed / 0 failed over 79,515 fixture cases, plus 11 harness unit tests (1 ignored, a pre-existing timing benchmark).cargo test --workspace --exclude 'ethrex-l2*' --exclude ethrex-prover --exclude ethrex-guest-program: 1,915 passed / 0 failed across 48 suites (11 ignored, all pre-existing).cargo clippy --workspace --all-targets -- -D warningsand the same intooling/: clean.cargo fmt --all -- --check, both workspaces: clean.Checklist
STORE_SCHEMA_VERSION(crates/storage/lib.rs) stays at 3, and that was checked rather than assumed: a persisted frame receipt genuinely does change shape, but only a chain that activates the pseudo-fork can hold one, no migration could rescue such a database because the same revision also changes the type-0x06transaction encoding, andcrates/storage/is untouched by this PR. The full reasoning is in the Description above.Dev-branch delivery to
frames-devnet-0skipped: the cherry-pick conflicts — needs a human (the loop never resolves conflicts).