Skip to content

feat(spec-specs,test-forks,tests): implement EIP-8141 frame transactions - #3936

Open
gurukamath wants to merge 42 commits into
forks/bogotafrom
eips/bogota/eip-8141
Open

gurukamath wants to merge 42 commits into
forks/bogotafrom
eips/bogota/eip-8141

Conversation

@gurukamath

Copy link
Copy Markdown
Contributor

Opening this PR as an alternative to #3878 but with an execution-specs branch instead of a fork branch so that all changes flow directly through to the PR without needing a manual push.

Description

Merge the eips/bogota/eip-8141 branch into forks/bogota. EIP-8141 (Frame
Transactions) is Scheduled for Inclusion in the Hegotá meta EIP (EIP-8081),
so the implementation moves from its prototyping branch onto the fork branch.

The branch is rebased onto the current forks/bogota tip and brings:

  • Spec: the type-6 frame transaction in ethereum.forks.bogota: frame
    decoding and static validity, the VERIFY/SENDER/PAYER/CALL/
    POST_TX frame modes, approval scope, the per-frame gas accounting and
    calldata floor settlement, the frame interpreter and introspection
    opcodes, the expiry verifier deployed as an ordinary contract, and the
    block execution gas accounting before refunds.
  • Spec tools: t8n loading of frame transactions (sender, fees, frames,
    signatures).
  • Testing framework: EIP8141 fork mixin for Bogota, frame transaction
    types and signing helpers, the frame transaction variant that re-fills
    eligible earlier-type tests as frame transactions (state, blockchain and
    engine_x formats), and client exception mappings for geth, Nethermind,
    besu, ethrex and Reth.
  • Tests: tests/bogota/eip8141_frame_transactions covering admission
    and static validity, target resolution, warmth, block access lists,
    introspection, the expiry verifier, the fork transition install, approval
    edge cases, blob-carrying frame transactions and value-moving frames.

Every commit on the branch was reviewed as a separate PR against it. On the
tip, just static passes and
fill --from Amsterdam --until Bogota tests/bogota/eip8141_frame_transactions
produces 875 fixtures.

Follow-ups once merged: rebase eips/bogota/eip-8250, eips/bogota/eip-8272,
eips/bogota/eip-7906 and devnets/bogota/1 onto forks/bogota; #3799
retargets to forks/bogota.

Related Issues or PRs

Part of #2829.

Checklist

  • Ran fast static checks to avoid CI fails, see Code Standards & Verifying Changes: just static
  • PR title has the form <type>(<area>): <description>, where <type> and <area> come from an appropriate C-<type>, respectively A-<area>, label. The title should match the target squash commit message.

Cute Animal Picture

Cute Animals - 1 of 1

SamWilsn and others added 30 commits October 24, 2026 10:25
* feat(specs): EIP-8141 transaction types

* WiP

* feat(specs): EIP-8141 frame transaction processing

Implement the type-6 frame transaction flow on the amsterdam fork,
separated from the regular flow from admission onwards:

- static validation, signature verification, intrinsic gas, and the
  two gas anchors (standard_gas_limit, max_gas) in
  transactions/frame_transaction.py
- consensus receipt types (FrameReceipt, FrameTransactionReceipt)
  and the 0x06 receipt codec arms in blocks.py
- TransactionEnvironment split into top_level_context/frame_context
  in vm/__init__.py
- admission through a dedicated check_frame_transaction in
  frame_processing.py; process_transaction dispatches frame
  transactions after the chain-id check
- frames execute as independent top-level calls in
  vm/frame_interpreter.py: per-frame gas meters, atomic batches,
  cross-frame warm journal, default verify code, approval rollback
- the six frame opcodes (TXPARAM, FRAMEDATALOAD, FRAMEDATACOPY,
  FRAMEPARAM, SIGPARAM, APPROVE) in vm/instructions/frame.py
- settlement anchored on the standard gas limit with the calldata
  floor binding through max_gas; the payer refund is
  max_cost - charged_fee; the frame transaction receipt reports
  status, gas, and logs per frame

Co-authored-by: lightclient <lightclient@protonmail.com>

* feat(spec-tools): adapt t8n for FrameTransaction

Co-authored-by: lightclient <lightclient@protonmail.com>

* feat(test-types): Add EIP-8141 support to testing framework

Co-authored-by: lightclient <lightclient@protonmail.com>

* feat(tests): EIP-8141 frame transaction tests

Co-authored-by: lightclient <lightclient@protonmail.com>

* fix(bogota): correct SIGPARAM copy operand order

EIP-8141 aligned the SIGPARAM (0xb4) copy operation's stack order with
CALLDATACOPY (ethereum/EIPs 4a9ad32c): after signatureIndex and param the
operands are memOffset, dataOffset, length. The implementation still popped
them in the old order (length, dataOffset, memOffset) — the reverse of its
own FRAMEDATACOPY. Swap to match the live spec.

* Restore minimal comment on SIGPARAM copy form

* fix: warm the sender for frame transactions

Frame transaction processing initializes the shared, once-per-tx warm
address set with the sender so its first EVM access is charged warm
(100) rather than cold (2600). The sender is loaded unconditionally for
the nonce check and, like a regular transaction's origin, must start
warm per EIP-2929 and EIP-3651.

* feat(spec): add EXPRIY_VERIFIER contract

* feat(tests): add introspection tests

test: cover the EIP-8141 introspection instructions

* feat(tests): add more introspection tests

* feat(tests): add frame transaction warmth tests

* fix(tests): restore EIP-8141 reference spec pin

* fix(spec): adapt frame transaction gas to ExecutionGas/StateGas newtypes

Upstream #3121 wrapped amsterdam gas values in ExecutionGas/StateGas
NewTypes; wrap the frame transaction call sites accordingly.

* feat(tests): add frame transaction validity tests

* feat(tests): more frame transaction validity tests

* fix(tests): fix broken egacy tests for frames

* feat(tests): add block access list cases for frame transactions

* feat(tests): frame-transaction block access list cases

Add EIP-8141 frame-transaction cases that pin the consensus-visible
EIP-7928 block access list across frame rollback paths: an atomic-batch
unroll and a frame revert drop the discarded storage write and re-file
the slot as a bare access; a skipped atomic-batch frame stays absent
from the BAL; and a sponsored transaction attributes the payer's fee
and the sender's nonce to distinct accounts. These paths are exercised
only as state tests today, so the BAL is unpinned for them.

* refactor(tests): tighten frame BAL cases and reuse module helpers

Build the frames with the module's `verify_frame`/`sender_frame`
helpers instead of hand-rolled `Frame` literals, matching the rest of
the suite.

Tighten the assertions so each case pins what it claims: the sponsored
transaction now asserts the sender has no balance change and the payer
no nonce bump, so a spurious BAL entry cannot pass on a correct post
state, and the atomic-batch cases assert the sender's nonce bump is
recorded whether or not the batch unrolls.

Derive the payer's post balance from the charged gas and fee per gas
rather than hardcoding it, and pin both on the transaction, so a gas
change surfaces as a gas mismatch instead of an opaque balance.

---------

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>

* feat(tests): additional gas helpers for frame transactions

* feat(tests): add frame target resolution test cases

* feat(tests): frame target resolution cases

Cover the three ways a frame's resolved target carries the empty code
hash: a precompile under each non-VERIFY mode and under VERIFY, and an
EIP-7702 authority whose designation is resolved at frame entry or
points at a precompile.

* feat(tests): discriminate the VERIFY target resolution split

Move the approvals of the precompile-target `VERIFY` case onto the
first frame and leave the frame under test approving nothing. With the
approvals on the frame under test, dispatching the precompile leaves
the transaction with no payment approver, so it is rejected either way
and the case rests on matching the exact rejection reason; with them
moved, dispatching makes the transaction valid.

Add a `VERIFY` frame whose target designates a precompile: its
resolved code is empty, as for the codeless target that does route to
the default code, but the target holds a designation and so must
follow it instead.

* refactor(tests): pair the warming frame with its receipt

The warming frame, its expected receipt and the designated address's
access charge each branched on `warm_delegate` separately, the receipt
through a comprehension over the frame list. Decide all three in one
place so the frame and its receipt cannot drift apart.

* feat(tests): frame gas that cannot afford the designation access

The designated account is read only once its access is paid for, so a
frame that cannot afford it leaves the account out of the block access
list as well as unexecuted.

* refactor(tests): use the frame entry gas calculator

Account the frame entry charges with `fork.frame_entry_gas_calculator()`
rather than summing `gas_costs` members, so the target and designation
access charges come from one place.

Pin the sender's nonce as unchanged in the rejected precompile-target
case, and move the unaffordable-designation case to the block access
list suite: the receipts cannot tell a resolution failure from a
failure inside the resolved code, since either forfeits the whole frame
gas limit, so assert the designated account absent from the BAL and the
target present as a bare access. Its frame gas is the largest that
still cannot afford the designation's access.

---------

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>

* feat(tests): create transaction tests from validity checks

* fix(specs,tests): add max gas checks and tests

plus some other fixes suggested in spencers review

* feat(tests): cover the frame-entry charge a target that is not alive adds

This covers the third charge `create_evm_from_frame` levies against a
frame's own gas before its EVM is built: the `NEW_ACCOUNT` state gas a
client levies when the frame's value transfer revives a target that is
not alive. `test_frame_target_entry_charge` covers the target's own
access and `test_delegated_target_entry_charge` the delegate's; this
covers the remaining one.

`test_dead_target_entry_charge` covers the case where a client enters a
`SENDER` frame moving value to an account that is not alive. The target
has no code, so the frame runs empty code and the receipt's `gas_used`
is the entry charge alone.

`NEW_ACCOUNT` is 120 state bytes, 183_600 gas, which is more than a
frame usually declares, so the affordable arm is only half the
behavior. The second arm gives the frame one gas less than the charge
and covers the case where a client cannot enter the frame at all: it
forfeits its whole gas limit, the target stays dead, and no transfer
log is emitted.

The case pins per-frame `gas_used` rather than the transaction total,
because the EIP-7623 calldata floor can dominate a small frame
transaction's total and hold it constant while a frame's own gas moves.

`frame_entry_gas_calculator` already takes `sends_value_to_dead_account`
and the test derives its expectations from it. Filled against the
reference implementation; both arms go red if the charge is not levied.
It came up while running a devnet.

Co-authored-by: ilitteri <ivan.litteri@lambdaclass.com>

---------

Co-authored-by: Guruprasad Kamath <guru241987@gmail.com>
Co-authored-by: lightclient <lightclient@protonmail.com>
Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
Co-authored-by: Daniil Ankushin <ankushin.daniil42@gmail.com>
Co-authored-by: Marc <Marchhill@users.noreply.github.com>
Co-authored-by: Ivan Litteri <67517699+ilitteri@users.noreply.github.com>
Co-authored-by: ilitteri <ivan.litteri@lambdaclass.com>
Two halves of the same EIP-8141 revision (ethereum/EIPs `9c915ee49`), which the
implementation here predates.

The static rule is new to both the spec and the tests: approval scope is
disallowed on every frame of an atomic batch, including its terminating frame -- a
frame belongs to a batch when it or its predecessor carries the flag. Keeping the
approval context constant across a batch means unrolling one can never withdraw an
execution approval that later SENDER frames rely on, and whether the transaction
sets a payer never depends on a batch outcome. Three cases cover it: the flagged
member, the terminating frame, and a frame after the terminator, which is outside
the batch and stays valid so the rule's upper boundary is pinned too.

The revert rule is tests-only; the implementation already behaves correctly and
this pins it. A frame that reverts discards the approval context it granted.
APPROVE exits its own call context, so a frame cannot approve and then revert
directly, but a nested call back into the frame's target can: the inner call
approves and returns, and the outer frame decides afterwards. Keeping the approval
does not merely leak it -- the payer stays bound while the max_cost collection it
performed is rolled back, so the transaction is valid and settles fees against an
account that paid nothing. The reverting case is paired with a returning control
case, which is what makes it meaningful: without the control, an implementation
that keeps the approval would pass the reverting case for the wrong reason, having
been rejected for never approving at all.
…3398)

* Cover the signature-validation precompiles being absent from the block access list

A frame transaction's protocol-validated signatures are checked before execution,
outside the EVM, so ecrecover and P256VERIFY are never called by the transaction
and must not appear in the block access list.

Nothing in state distinguishes the two behaviours -- a precompile has no storage,
balance or nonce to change -- but the block access list is committed to in the
header, so an implementation that validates signatures by dispatching through its
own EVM adds the precompile as a touched address, produces a different block
access list hash, and has its block rejected while every other result matches.
That is what makes it worth a fixture rather than a comment.

The transaction carries one signature of each protocol-validated scheme so both
precompiles are exercised, and asserts each is absent. Flipping either assertion
to expect the address present fails, so the absence is really being checked.

* Reuse the shared precompile address constants and take the default frame gas

The ecrecover and P256VERIFY addresses are already defined in
tests/frontier/precompiles/spec.py and
tests/osaka/eip7951_p256verify_precompiles/spec.py, so import them instead of
redefining the literals.

The sender frame targets Op.STOP and writes nothing, so it does not need
WRITE_FRAME_GAS, which is documented as what a writing frame requires beyond
the framework default.
Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
…gota (#3418)

Co-authored-by: Felix H <felix314159@users.noreply.github.com>
This implements the spec updates from this PR - ethereum/EIPs#12226

* fix(eip-8141): account block execution gas before refund

* test(eip-8141): bump reference spec for EIP-7778 integration

* fix(eip-8141): settle refund and floor accounting together

* test(eip-8141): derive refund floor cases dynamically

* style(test): format gas settlement test

* fix(tests): static checks

* fix(tests): pin search transaction nonce


---------

Co-authored-by: Guruprasad Kamath <guru241987@gmail.com>
* feat(tests/amsterdam): pin frame-tx spent gas across a refill rollback

Add two EIP-8141 state tests that a storage refill returned to a frame
by a later frame's rollback lands in the transaction's spent gas, not
only in the per-frame receipt. One drives an atomic batch that unrolls
after a batch frame refills a pre-batch receipt; the other reverts the
refilling frame itself. Both assert every frame's state gas and the
transaction's cumulative gas used.

* chore(tests): move refill settlement tests to test_gas_settlement

---------

Co-authored-by: Guruprasad Kamath <guru241987@gmail.com>
…mapper (#3467)

NethermindExceptionMapper had no TYPE_6_* entries, so every frame
transaction rejection fell through to the TYPE_3_TX_WITH_FULL_BLOBS
catch-all, which matches the client's generic "Transaction <n> is not
valid: <cause>" decode wrapper and discards the cause.

Add the format, signature and execution wordings, and the decode-time
and fee-overflow ones the same catch-all was swallowing. The mapper is
additive, so these narrow what a fixture matches without changing any
message that already resolved.

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
#3500)

* chore(tests): pin spec to the latest

This does not require an implementation update or additional tests since the implementation already exihibits this behaviour

* feat(tests): refill legacy state tests as frame transactions

A tree-level tests/conftest.py that parametrizes every state test
with a second, frame-transaction shape. The frame variant rewrites
the test's transaction into the canonical [VERIFY, SENDER] frame
pair — carrying fees (the default legacy gas price included), blobs,
and non-gas receipt/header pins through, and stripping gas pins,
which the fixture's post-state root still covers via the fee
balances — and refills it otherwise unchanged, gated on the fork
supporting transaction type 6.

The SENDER frame's budgets are derived per test: execution maximized
against the EIP-7825 cap net of the calldata intrinsic, state fitted
under the block gas limit. A test's smaller block gas limit is
raised to the framework default for the frame variant, since the
two frame gas dimensions cannot spill into each other the way the
legacy pool does.

Unconvertible transactions (types 1 and 4, creation, access lists,
explicit gas limits, reservoir semantics, delegated senders) are
reported under a dedicated frame-unconvertible category: no progress
characters, one grouped per-reason summary block. A test pinning the
sender's post balance fails its frame variant with a message
directing to the receipt-pin idiom; test_value_move_to_precompiles
is rewritten to that idiom accordingly. Oracles that genuinely
depend on the transaction type opt out via the new
frame_tx_incompatible marker — first user: test_all_opcodes, whose
invalid-opcode expectation for TXPARAM (0xB0) only holds outside a
frame transaction. The ported_static skip-list normalizer learns to
strip the transaction-shape id segments.

Verified at Bogota over the full tree (state tests, not slow):
18,492 passed, 0 failed; 4,402 frame variants filled green; the
amsterdam directory's 522 blockchain-fixture twins have post storage
identical to their legacy counterparts; pre-Bogota fills are
byte-identical with and without the conftest.

* refactor(tests): fill frame-tx variants as labeled fixture formats

Replace the tree-level conftest's parametrize/fixture-override
machinery with the test-variants mechanism: FrameTransactionVariant,
a LabeledFixtureFormat subclass, labels every fill-phase state-test
format as <format_id>_frame_tx, and StateTest.generate() applies the
transaction rewrite itself when handed the variant. The converter
moves into the testing package next to StateTest, base fixture ids
stay exactly as authored (the -non_frame_tx segment disappears), and
the same-name state_test fixture shadowing is gone.

The variant vetoes itself where the old conftest skipped: at
collection for non-frame forks (supports_fork) and for tests marked
exception_test or frame_tx_incompatible — now seen at param level
too, which the function-level check missed — and at fill time, via
runtime skips, for unconvertible transactions. The engine_x and
stateful formats are excluded: their pre-alloc phases hash the
genesis of the untransformed test. Each variant carries its own
transition-tool cache key so a frame transaction never shares the
base format's cached t8n output, and the filler's sort key learns
the cache key so interleaved base/variant items stop clearing the
single-key cache.

The frame-unconvertible skip reporting moves from the conftest into
a filler plugin module, registered tryfirst: pytest-custom-report
claims every skipped report in the firstresult race, and only a
conftest — registered last — used to win without it.

Framework unit tests follow the mechanism: the duplicate-cache-key
verifier treats an empty key as the caching opt-out it is, the
derived-labels test counts only non-variant labels, the forks-plugin
tests consult the label's supports_fork rather than the wrapped
format's, and the collect-only test expects the discarded variants
in pytest's pre-discard collection count.

Verified at Bogota: fixture content is identical to the conftest
mechanism across the amsterdam tree, the DUPN/SWAPN/EXCHANGE
directory's full formats, and the ported skip-listed files, with a
100% t8n cache hit rate; pre-frame Amsterdam fills are byte-identical
to the bare upstream base.

* fix(tests): preserve state-test context in frame variants

* fix(tests): preserve chain ID in frame transaction variants

* fix(tests): preserve block gas limit in frame variants

---------

Co-authored-by: spencer <spencer.tb@ethereum.org>
…fork transition (#3539)

* feat(test-specs,tests): exercise the EIP-8141 expiry verifier install at the fork transition

EIP-8141 installs the expiry verifier's runtime code at 0x...8141 when the
fork activates, and leaves the account's nonce and balance as they were
(EIPs@b75cbe6, "Expiry verifier contract"; `apply_fork` in the Amsterdam
spec module on this branch). No fixture exercised that install: every
EIP-8141 test starts at Bogota with the account already in the genesis
allocation, so a client that installs it with a nonce of one passes the
whole suite and diverges on the state root of the activation block of a
live network.

Three things stood in the way of a transition fixture:

- there was no transition fork into Bogota;
- `make_genesis` seeds `transitions_to().pre_allocation_blockchain()`, so
  even a transition fixture would have carried the code at genesis;
- nothing in the filler applies a fork's activation-time state change
  (`apply_fork` is only called by `ethereum_spec_tools.sync`), which no
  fork since the DAO fork has needed.

Add `BaseFork.activation_code_installs()`: the runtime code a fork writes
when it activates, keyed by address. `EIP8141` moves the verifier from
`pre_allocation_blockchain` to this hook; state tests keep `pre_allocation`,
having no activation to cross. `Alloc.with_installed_code` applies such
installs changing only the code. The blockchain filler applies the genesis
fork's installs at genesis and, in `generate_block_data`, the installs new
to a block's fork relative to its parent's fork before the block executes.
The hive `execute` genesis and the engine_x pre-alloc groups apply the
genesis-side installs the same way. Add `AmsterdamToBogotaAtTime15k`.

Add `test_fork_transition.py`. A probe records `EXTCODESIZE` of the
verifier per block, none before the fork and the code from the fork block
on, with the account absent, funded by a pre-fork transfer, or already
present with a nonce and balance that must survive the install; and a
frame transaction carrying an expiry frame executes in the activation
block itself. A pre-fork type-6 rejection test is not possible while
Bogota shares Amsterdam's spec module.

No pre-existing fixture changes: filling
tests/amsterdam/eip8141_frame_transactions at Bogota with all formats on
the unmodified and on the modified tree yields 943 hash-identical fixtures;
the new tests add 21 (7 cases in 3 formats). `just static` and the forks,
specs and fixtures unit tests pass.

* docs(tests): describe what the EIP-8141 transition test pins, not what a wrong install would do

The docstring of the install test explained the nonce check by describing
one incorrect implementation. State the check itself instead: the
post-state pins nonce, balance and code, so an install that writes anything
other than the code fails.

* refactor(test-specs): compare activation installs by Address

The fork-block install diff in generate_block_data looked up the parent
fork's installs with whatever key type each hook returned. Normalising
both sides to Address means an int key on one fork and an Address key on
another still compare equal, instead of re-installing code the parent
fork already had.

* feat(tests): pin storage and the block access list in the EIP-8141 install tests

Add a case where the verifier address already holds storage before the
fork, so the post-state pins that only the code changes and the storage
survives, matching the hook's contract of a code-only install.

Assert the block access list of every block: the verifier appears only
through the transactions that touch it and never with a code change, in
the fork block included. The install is not a block-level operation and
must leave no trace there.

Mark the module valid_at_transition_to("Bogota") rather than the EIP
name, and document two limits in the module docstring: these tests cover
the verifier install, not the activation block as a whole, and a
transition test that needs pre-fork Amsterdam rules is not possible while
EIP-8141 lives inside the amsterdam spec package and Bogota is only a
test-side label for it.
* feat(consume): map Besu EIP-8141 frame transaction exceptions

Besu rejects invalid type-6 frame transactions with its own error
strings, none of which BesuExceptionMapper knew about, so every
EIP-8141 negative test failed with "Undefined exception message"
rather than comparing the exception.

Signed-off-by: jflo <justin+github@florentine.us>

* feat(consume): map Besu EIP-8141 frame transaction exceptions

    Besu rejects invalid type-6 frame transactions with its own error
    strings, none of which BesuExceptionMapper knew about, so every
    EIP-8141 negative test failed with "Undefined exception message"
    rather than comparing the exception.

Signed-off-by: jflo <justin+github@florentine.us>
EIP branches that each rewrite the Bogota fork docstring conflict when the devnet workflow merges them, so this branch leaves it as forks/bogota has it; the EIP is listed there when it lands.
…ing inside a frame (#3757)

ethereum/EIPs#12458 adds the approval context to what EIP-8141 journals
with state, alongside the state-gas ownership records and the frame
receipt edits: "Reverting a call, frame, or atomic batch restores all of
them to the corresponding checkpoint." The spec already restores the frame
context on every failed message call, but the suite only reverted frames
and atomic batches, so a client that restored nothing on a failed call
inside a successful frame passed it.

test_call_revert_above_approve_discards_the_approval nests an APPROVE two
calls below a frame that succeeds and reverts the call in between: the
approval goes with it, leaving the transaction without a payer, and the
frame can approve again afterwards; returning instead is the control.
test_call_revert_restores_refilled_receipt reverts a call that refilled an
earlier frame's receipt and checks the receipt and the executing frame's
pool. test_reversion_restores_charge_ownership consumes a slot's charge
ownership record in a reverted call, a reverted frame and an unrolled
batch, and checks that a later clear still refills the frame that paid.

The reference spec pin moves to 7752622050, which includes #12458 and the
parameter names #3754 already uses.
… the minimum data (#3784)

Co-authored-by: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com>
Co-authored-by: 蔡佳誠 Louis Tsai <72684086+LouisTsai-Csie@users.noreply.github.com>
* feat(tests): pin the memory charge of a refused EIP-8141 APPROVE

A refused APPROVE still pays the memory expansion of its return-data region, charged before the checks that refuse it; one case per refusal: wrong caller, scope beyond the mask, scope outside the frame's flags, payer already set.

* feat(tests): charge creation of an EIP-8141 sender emptied mid-tx

A sender an earlier frame spent down to empty does not exist under EIP-8037's rule, so APPROVE charges its account creation; the creation tests now cover it and pin the budget at the charge and one unit below.

* feat(tests): leave an EIP-8141 delegate unread on a failed transfer

A SENDER frame whose value exceeds the balance fails before dispatch, so a delegated recipient's delegate is neither charged nor recorded in the BAL.

* feat(tests): accept an EIP-8141 transaction from the zero address

The static rules only check the sender's length, so a zero-address sender whose code approves is valid; pinned as a state test and a transaction test.

* feat(tests): delegate an EIP-8141 failed transfer to a precompile

A precompile delegate is always warm, so a client that reads the delegate on a failed transfer shows it only as an extra block access list entry. The contract case alone catches that read through its cold access gas.

* refactor(tests): delegate a failed EIP-8141 transfer to every precompile

The precompile case moves into its own test over with_all_precompiles, sharing the transaction with the contract case through a helper, since a client can single out some precompiles.

* refactor(tests): take EIP-7708 transfer logs from its spec module

The value transfer tests rebuilt the EIP-7708 log address, topic and log helper that the 7708 suite's spec module already defines.
ilitteri and others added 9 commits October 24, 2026 10:27
Cover blob-carrying frame transactions beyond the single-transaction
frame variants of the Cancun blob tests: BLOBHASH and the TXPARAM blob
fields in DEFAULT, SENDER and VERIFY frames; frame and EIP-4844
transactions sharing the block blob limit; rejection of a block body
carrying the network wrapper; and execute-only blob_transaction_test
cases that submit the wrapper over eth_sendRawTransaction and read the
blobs back through engine_getBlobsV*.

In execute mode a blob-carrying frame transaction now defaults its blob
fee cap as a type-3 one does, and set_gas_price and
signer_minimum_balance account for it. Frame transactions without blobs
keep a blob fee cap of 0, and filled fixtures do not change.

Backport of #3824, merged into devnets/bogota/1 as 6e38ded, without
its EIP-7906 POST_TX BLOBHASH test, which lands on eips/bogota/eip-7906
separately.

Co-authored-by: Guruprasad Kamath <guru241987@gmail.com>
The frame transaction rewrite changes only the transaction and the
post-state expectations, never the pre-allocation or the environment,
so a variant derives the same genesis as its base test and can join
its pre-allocation group. Give every format except the stateful one a
frame_tx variant, and add a test that the rewrite keeps the genesis.
…ns (#3826)

The EIP-8141 share of #3826, which landed on devnets/bogota/1 and in no
EIP branch, so every devnet rebuild had to re-pick it. The EIP-8250 and
EIP-7906 messages go to their own branches.
* feat: add frame transaction helper

* refactor: apply helper to all the test implementation

* refactor(tests): drop the EIP-8141 frame gas budget that restates the default

* refactor(tests): drop the EIP-8141 frame pins the helper derives

* test(tests): pin the EIP-8141 deployment and transition frame gas
…ges for `bogota-devnet-2` (#3892)

Co-authored-by: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com>
The shared devnet release feature fills through Bogota on this branch,
so its build matrix ends with the bogota range. The assertion that it
ends at Amsterdam came in with #3597 against the fork branch's config,
where the feature stops at Amsterdam.
@codecov

codecov Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.98540% with 44 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.12%. Comparing base (97166d9) to head (8992fc5).
⚠️ Report is 22 commits behind head on forks/bogota.

Files with missing lines Patch % Lines
...rc/ethereum/forks/bogota/vm/instructions/system.py 41.66% 10 Missing and 4 partials ⚠️
src/ethereum/forks/bogota/vm/frame_interpreter.py 94.27% 5 Missing and 6 partials ⚠️
...eum/forks/bogota/transactions/frame_transaction.py 97.06% 5 Missing and 3 partials ⚠️
src/ethereum/forks/bogota/vm/__init__.py 91.66% 4 Missing and 4 partials ⚠️
src/ethereum/forks/bogota/frame_processing.py 97.36% 1 Missing and 1 partial ⚠️
src/ethereum/forks/bogota/transactions/__init__.py 94.73% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@               Coverage Diff                @@
##           forks/bogota    #3936      +/-   ##
================================================
+ Coverage         95.10%   95.12%   +0.02%     
================================================
  Files               673      677       +4     
  Lines             40904    41914    +1010     
  Branches           3778     3941     +163     
================================================
+ Hits              38903    39872     +969     
- Misses             1393     1417      +24     
- Partials            608      625      +17     
Flag Coverage Δ
unittests 95.11% <95.89%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

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

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

@bt-recneps bt-recneps left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mutated 30 EIP-8141 spec rules, six survive every fill, also uncaught are payment before execution, exact payer balance and the TSTORE reset :(

Reviewed in 14m 49s

@pytest.mark.parametrize(
"call_opcode,scope",
[
pytest.param(Op.CALL, Spec.APPROVE_PAYMENT, id="caller_not_target"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

caller_not_target also fails payer_already_set and passes with APPROVE's ADDRESS != resolved_target check deleted, so run its inner CALL before any payer is set.

Comment on lines +367 to +368
if len(signature.message) != 0:
return FrameStatus.FAILURE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Deleting this empty-msg check passes every fill, so add a default-code VERIFY case whose index-0 entry is signed_digest_entry(sender.key), expecting TYPE_6_INVALID_FRAME_EXECUTION.

The EIP-8141 share of #3926, which landed on devnets/bogota/2 and in no
EIP branch, so every devnet rebuild would have had to re-pick it.
@Mannan-Goyal

Mannan-Goyal commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Re the bt-recneps review above: I ran these mutants against the tests in #3917 (which targets this branch). It catches the 3 surviving mutants from the inline comments and the 3 uncaught rules from the summary. The current tests on eips/bogota/eip-8141 catch none of them.

Mutant Existing tests (974 fill cases) #3917's test files (2,306 fill cases)
APPROVE with the ADDRESS != resolved_target check deleted 0 fail 9 fail: test_approval_edges.py::test_approve_in_library_code, test_approve_reached_through_another_contract
VERIFY frame run with is_static=False 0 fail 132 fail: test_verify_frame_static.py (SSTORE, TSTORE, LOGs, CREATE/CREATE2, SELFDESTRUCT and value CALL, both directly and from a nested call)
default code with the empty-msg check deleted 0 fail 6 fail: test_default_code_signatures.py::test_default_code_entry_selection[sender_entry_with_explicit_msg] and [payer_entry_with_explicit_msg]
payment approved before execution 0 fail 6 fail: test_approval_edges.py::test_split_approval_order[payment_then_execution], test_default_code_approval_refusals[payment_before_execution]
payer balance exactly equal to the max cost 0 fail 19 fail: the balance_at_max_cost cases in test_approve_balance.py
transient storage not cleared between frames 0 fail 33 fail: test_transient_storage.py

caller_not_target itself is unchanged in #3917, so the change suggested for it still applies.

How I ran it: each mutation applied to src/ on #3917 merged with the current eips/bogota/eip-8141, then fill --until Bogota on #3917's test files and, separately, on the existing test dir. Without mutations both runs pass. The counts are fill cases, so one test can count more than once (one per fixture format).

The summary doesn't show the exact edits for the last three, so I used the simplest edit that breaks each rule: deleted the "execution approved first" check, changed < to <= in the max-cost check, and removed transient_storage.clear(). I read the six as the 3 from the inline comments plus the 3 named in the summary; if there are others, I haven't checked them.

…torage tests (#3939)

* feat(tests): add EIP-8141 VERIFY-frame static context and transient storage tests

* refactor(tests): share CALL_VALUE and drop an always-true TLOAD check

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.