Skip to content

feat(fork-types): add ExecutionGas and StateGas NewType wrappers to amsterdam EVM - #3121

Merged
spencer-tb merged 1 commit into
ethereum:forks/amsterdamfrom
mandeepmourya007:refactor/gas-newtypes
Aug 5, 2026
Merged

spencer-tb merged 1 commit into
ethereum:forks/amsterdamfrom
mandeepmourya007:refactor/gas-newtypes

Conversation

@mandeepmourya007

@mandeepmourya007 mandeepmourya007 commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Description

Applies the ExecutionGas and StateGas NewType wrappers (introduced in
fork_types.py by #3037) to all EVM gas fields and constants across the
amsterdam fork, preventing accidental mixing of state gas with execution gas
at the type level.

Changes:

  • All GasCosts constants typed as ExecutionGas (blob-fee, byte-count and
    block-limit constants stay Uint); StateGasCosts already typed as
    StateGas
  • GasMeter fields typed: gas_left, state_gas_left,
    state_gas_baseline, state_gas_spilled, state_gas_committed_spill
  • Message, TransactionEnvironment, MessageCallOutput, GenericCall,
    MessageCallGas and ExtendMemory gas fields typed
  • EvmGasAllocation and the per-dimension TransactionGasSettlement fields
    typed
  • Signatures updated: the gas-meter helpers (charge_state_gas, spill
    commit/restore, withhold_create_gas, drain_state_gas_reservoir,
    restore_child_gas, tx_state_gas_used), max_message_call_gas,
    calculate_delegation_cost and settle_transaction_gas

Rebased onto forks/amsterdam after the regular→execution gas rename
(#3238, #3263) and the GasMeter refactor, so the wrapper is now named
ExecutionGas and the typing covers the meter and settlement helpers.

Related Issues or PRs

Closes #3015. Follows on from #3037.

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

Typing cat

@spencer-tb
spencer-tb force-pushed the refactor/gas-newtypes branch from 546a7e6 to 356a1a8 Compare July 30, 2026 12:59
@spencer-tb spencer-tb changed the title feat(fork-types): add RegularGas and StateGas NewType wrappers to ams… feat(fork-types): add ExecutionGas and StateGas NewType wrappers to amsterdam EVM Jul 30, 2026
@spencer-tb
spencer-tb requested a review from gurukamath July 30, 2026 14:35
@spencer-tb

Copy link
Copy Markdown
Contributor

@gurukamath I updated this PR with the execution naming change, what do you think?

Should we add this and do we need to backport the execution gas change?

@codecov

codecov Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.50%. Comparing base (5bb2a51) to head (3d44ead).
⚠️ Report is 1 commits behind head on forks/amsterdam.

Additional details and impacted files
@@               Coverage Diff                @@
##           forks/amsterdam    #3121   +/-   ##
================================================
  Coverage            93.49%   93.50%           
================================================
  Files                  624      624           
  Lines                37056    37070   +14     
  Branches              3394     3394           
================================================
+ Hits                 34647    34661   +14     
  Misses                1653     1653           
  Partials               756      756           
Flag Coverage Δ
unittests 93.50% <100.00%> (+<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.

@gurukamath gurukamath 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.

Some minor nits. I think the branch also needs a rebase to avoid merge conflicts

gas_price: Uint
gas: Uint
state_gas_reservoir: Uint
gas: ExecutionGas

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.

Should we change the types in BlockOutput as well?

gas_meter.gas_left += child_meter.gas_left
gas_meter.state_gas_left += child_meter.state_gas_left
gas_meter.state_gas_spilled += child_meter.state_gas_spilled
gas_meter.gas_left = ExecutionGas(

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.

This should work in its current form since they are all the same type, right? Also applies to some of the other gas helper functions I think

Comment thread src/ethereum/forks/amsterdam/vm/gas.py Outdated
if gas_meter.gas_left < amount:
raise OutOfGasError
gas_meter.gas_left -= amount
gas_meter.gas_left = ExecutionGas(gas_meter.gas_left - amount)

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.

I feel like the types should show up in the function signatures instead of a later cast in the body. Should amount be ExecutionGas? Applies for the other gas functions as well

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.

GasCosts.CALL_VALUE and Uint(0) have different types here

@spencer-tb
spencer-tb force-pushed the refactor/gas-newtypes branch from 356a1a8 to 3d44ead Compare August 5, 2026 10:23
@spencer-tb
spencer-tb dismissed gurukamath’s stale review August 5, 2026 12:01

Changes are applied.

@spencer-tb
spencer-tb merged commit f1aa970 into ethereum:forks/amsterdam Aug 5, 2026
24 checks passed
gurukamath added a commit to SamWilsn/eth1.0-specs that referenced this pull request Aug 10, 2026
Upstream ethereum#3121 wrapped amsterdam gas values in ExecutionGas/StateGas
NewTypes; wrap the frame transaction call sites accordingly.
ilitteri pushed a commit to lambdaclass/execution-specs that referenced this pull request Aug 11, 2026
…msterdam EVM (ethereum#3121)

Co-authored-by: Guruprasad Kamath <48196632+gurukamath@users.noreply.github.com>
ilitteri pushed a commit to lambdaclass/execution-specs that referenced this pull request Aug 11, 2026
Upstream ethereum#3121 wrapped amsterdam gas values in ExecutionGas/StateGas
NewTypes; wrap the frame transaction call sites accordingly.
gurukamath added a commit that referenced this pull request Aug 13, 2026
* 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>
gurukamath added a commit that referenced this pull request Aug 13, 2026
* 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>
ilitteri added a commit to lambdaclass/execution-specs that referenced this pull request Aug 13, 2026
…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>
gurukamath added a commit that referenced this pull request Aug 19, 2026
* 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>
spencer-tb pushed a commit that referenced this pull request Aug 22, 2026
* 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>
gurukamath added a commit that referenced this pull request Aug 26, 2026
* 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>
gurukamath added a commit that referenced this pull request Aug 26, 2026
* 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>
gurukamath added a commit that referenced this pull request Sep 2, 2026
* 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>
gurukamath added a commit that referenced this pull request Sep 3, 2026
* 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>
gurukamath added a commit that referenced this pull request Sep 3, 2026
* 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>
gurukamath added a commit that referenced this pull request Sep 4, 2026
* 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>
gurukamath added a commit that referenced this pull request Sep 4, 2026
* 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>
spencer-tb pushed a commit to spencer-tb/execution-specs that referenced this pull request Sep 28, 2026
…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>
spencer-tb pushed a commit to spencer-tb/execution-specs that referenced this pull request Sep 28, 2026
…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>
spencer-tb pushed a commit to spencer-tb/execution-specs that referenced this pull request Sep 29, 2026
…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>
spencer-tb pushed a commit that referenced this pull request Oct 5, 2026
* 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>
spencer-tb pushed a commit to AnkushinDaniil/eth1.0-specs that referenced this pull request Oct 6, 2026
…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>
gurukamath added a commit that referenced this pull request Oct 9, 2026
* 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>
spencer-tb pushed a commit to spencer-tb/execution-specs that referenced this pull request Oct 13, 2026
…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>
gurukamath added a commit that referenced this pull request Oct 13, 2026
* 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>
gurukamath added a commit that referenced this pull request Oct 21, 2026
* 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>
gurukamath added a commit that referenced this pull request Oct 21, 2026
* 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>
gurukamath added a commit that referenced this pull request Oct 23, 2026
* 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>
gurukamath added a commit that referenced this pull request Oct 24, 2026
* 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>
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.

Add newtype wrappers for StateGas and RegularGas

3 participants