Skip to content

feat(specs,tests): type EIP-8250 nonce keys by their most significant byte - #3724

Closed
spencer-tb wants to merge 23 commits into
ethereum:eips/bogota/eip-8250from
spencer-tb:eip-8250-nonce-type
Closed

spencer-tb wants to merge 23 commits into
ethereum:eips/bogota/eip-8250from
spencer-tb:eip-8250-nonce-type

Conversation

@spencer-tb

@spencer-tb spencer-tb commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Description

Defer this for a future devnet...

Implements ethereum/EIPs#12352 on eips/bogota/eip-8250: a nonce key's most significant byte is its type. Keys of a reserved type are rejected at decode, and a binary key only accepts sequence zero on an unused slot and is written once. The tests re-derive the key boundaries for typed keys and cover general, binary and reserved types, including slot separation between types and binary single use within a block.

Related Issues or PRs

Checklist

  • Ran fast static checks to avoid CI fails, see Code Standards & Verifying Changes: just static
  • PR title has the form <type>(<area>): <title>, 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

Put a link to a cute animal picture inside the parenthesis-->

SamWilsn and others added 23 commits October 5, 2026 23:50
…3047)

* 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 ethereum#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>
…#3393)

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.
…thereum#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.
…reum#3407)

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
…gota (ethereum#3418)

Co-authored-by: Felix H <felix314159@users.noreply.github.com>
…m#3443)

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>
…m#3487)

* 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 (ethereum#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>
ethereum#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 (ethereum#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>
…ions (ethereum#3683)

Co-authored-by: spencer-tb <spencer.tb@ethereum.org>
@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (eips/bogota/eip-8250@ae5c566). Learn more about missing BASE report.

Additional details and impacted files
@@                   Coverage Diff                   @@
##             eips/bogota/eip-8250    #3724   +/-   ##
=======================================================
  Coverage                        ?   94.93%           
=======================================================
  Files                           ?      677           
  Lines                           ?    41943           
  Branches                        ?     3968           
=======================================================
  Hits                            ?    39819           
  Misses                          ?     1470           
  Partials                        ?      654           
Flag Coverage Δ
unittests 94.93% <100.00%> (?)

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.

@spencer-tb

Copy link
Copy Markdown
Contributor Author

deferred until more discussion ..

@spencer-tb

Copy link
Copy Markdown
Contributor Author

Closing in favor of ethereum/EIPs#12467.

@spencer-tb spencer-tb closed this Oct 12, 2026
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.

8 participants