Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions src/ethereum/forks/amsterdam/utils/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from ethereum.state import Address

from ..state_tracker import get_account, get_code
from ..state_tracker import get_account
from ..transactions import Transaction
from ..vm import BlockEnvironment, Message, TransactionEnvironment
from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS
Expand Down Expand Up @@ -63,9 +63,7 @@ def prepare_message(
elif isinstance(tx.to, Address):
current_target = tx.to
msg_data = tx.data
code = get_code(
tx_env.state, get_account(tx_env.state, tx.to).code_hash
)
code = None
code_address = tx.to
else:
raise AssertionError("Target must be address or empty bytes")
Expand Down
2 changes: 1 addition & 1 deletion src/ethereum/forks/amsterdam/vm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ class Message:
value: U256
data: Bytes
code_address: Optional[Address]
code: Bytes
code: Optional[Bytes]
depth: Uint
should_transfer_value: bool
is_static: bool
Expand Down
12 changes: 6 additions & 6 deletions src/ethereum/forks/amsterdam/vm/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,6 @@ def prepare_dispatch(evm: Evm) -> None:
)
else:
message.code = recipient_code
evm.code = message.code
evm.valid_jump_destinations = get_valid_jump_destinations(message.code)


def process_message(message: Message) -> Evm:
Expand All @@ -328,16 +326,14 @@ def process_message(message: Message) -> Evm:
if message.depth > STACK_DEPTH_LIMIT:
raise StackDepthLimitError("Stack depth limit reached")

code = message.code
valid_jump_destinations = get_valid_jump_destinations(code)
evm = Evm(
pc=Uint(0),
stack=[],
memory=bytearray(),
code=code,
code=Bytes(b""),
gas_left=message.gas,
state_gas_left=message.state_gas_reservoir,
valid_jump_destinations=valid_jump_destinations,
valid_jump_destinations=set(),
logs=(),
refund_counter=0,
running=True,
Expand Down Expand Up @@ -374,6 +370,10 @@ def process_message(message: Message) -> Evm:
evm.error = error
return evm

assert message.code is not None
evm.code = message.code
evm.valid_jump_destinations = get_valid_jump_destinations(message.code)

snapshot = copy_tx_state(tx_state)

# Execute message code and handle errors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,10 @@ def test_set_delegation_oog_charge_point(
state. The sender pays the full ``gas_limit`` and its nonce is not
rolled back.

The recipient and both authorities were accessed before the halt
(the recipient at inclusion, the authorities during validation), so
per EIP-7928 all three must still appear in the block access list,
with no recorded changes.
Both authorities are read during authorization validation before
the halt, so per EIP-7928 they still appear in the block access
list with no recorded changes. The recipient is only loaded by the
top-frame dispatch, which the halt precedes, so it must be absent.
"""
gas_costs = fork.gas_costs()
sender_initial_balance = 10**18
Expand Down Expand Up @@ -230,10 +230,11 @@ def test_set_delegation_oog_charge_point(
}

# An implementation recording accesses only for dispatched frames
# would drop these entries and fork on the BAL hash.
# would drop the authority entries; one recording the recipient at
# inclusion would add it. Either forks on the BAL hash.
expected_block_access_list = BlockAccessListExpectation(
account_expectations={
recipient: BalAccountExpectation.empty(),
recipient: None,
first.authority: BalAccountExpectation.empty(),
second.authority: BalAccountExpectation.empty(),
}
Expand Down Expand Up @@ -284,9 +285,9 @@ def test_set_delegation_oog_rolls_back_first_auth(
The creation-first case is covered by
``test_set_delegation_oog_charge_point[new_account]``.

The recipient and both authorities were accessed before the halt,
so per EIP-7928 all three must still appear in the block access
list, with no recorded changes.
Both authorities are read during validation before the halt and
stay in the block access list with no recorded changes; the
recipient, never loaded before the halt, must be absent.
"""
gas_costs = fork.gas_costs()
sender_initial_balance = 10**18
Expand Down Expand Up @@ -330,7 +331,7 @@ def test_set_delegation_oog_rolls_back_first_auth(

expected_block_access_list = BlockAccessListExpectation(
account_expectations={
recipient: BalAccountExpectation.empty(),
recipient: None,
first.authority: BalAccountExpectation.empty(),
second.authority: BalAccountExpectation.empty(),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
Initcode,
Op,
RecipientType,
StateTestFiller,
Transaction,
Withdrawal,
compute_create_address,
Expand All @@ -31,6 +32,10 @@
)

from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702
from ..eip2780_reduce_intrinsic_tx_gas.helpers import (
AuthorizationAction,
build_authorization,
)
from .spec import ref_spec_7928

REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path
Expand Down Expand Up @@ -547,6 +552,92 @@ def test_bal_7702_top_frame_delegation_oog(
)


@pytest.mark.parametrize(
"outcome",
[
pytest.param("oog", id="oog_at_authorization_charge"),
pytest.param("success", id="success"),
],
)
def test_bal_7702_recipient_excluded_on_authorization_oog(
fork: Fork,
pre: Alloc,
state_test: StateTestFiller,
outcome: str,
) -> None:
"""
Ensure ``tx.to`` enters the BAL only when authorization processing
completes.

The single authorization is starved at its opening ``NEW_ACCOUNT``
charge, halting the transaction before the top-frame dispatch loads
the recipient: the recipient must be absent from the BAL, while the
authority -- read during authorization validation -- stays in it
with no recorded changes.
"""
sender = pre.fund_eoa()
recipient = pre.deploy_contract(code=Op.STOP)

auth = build_authorization(pre, AuthorizationAction.CREATES_ACCOUNT)
authorization_list = [auth.authorization]

intrinsic_regular = fork.transaction_intrinsic_cost_calculator()(
recipient_type=RecipientType.CONTRACT,
authorization_list_or_count=authorization_list,
return_cost_deducted_prior_execution=True,
)

recipient_expectation: BalAccountExpectation | None
expected_authority: Account | None
if outcome == "oog":
# The authorization runs out at its opening NEW_ACCOUNT state
# charge, drawn from gas_left under the zero state reservoir.
gas_limit = intrinsic_regular + fork.gas_costs().NEW_ACCOUNT - 1
recipient_expectation = None
authority_expectation = BalAccountExpectation.empty()
expected_authority = auth.original_account
else:
top_frame_regular = fork.transaction_top_frame_gas_calculator()(
recipient_type=RecipientType.CONTRACT,
authorizations=authorization_list,
)
top_frame_state = fork.transaction_top_frame_state_gas(
recipient_type=RecipientType.CONTRACT,
authorizations=authorization_list,
)
gas_limit = intrinsic_regular + top_frame_regular + top_frame_state
recipient_expectation = BalAccountExpectation.empty()
authority_expectation = BalAccountExpectation(
nonce_changes=[BalNonceChange(block_access_index=1, post_nonce=1)],
code_changes=[
BalCodeChange(
block_access_index=1,
new_code=auth.applied_account.code,
)
],
)
expected_authority = auth.applied_account

tx = Transaction(
sender=sender,
to=recipient,
authorization_list=authorization_list,
gas_limit=gas_limit,
)

state_test(
pre=pre,
tx=tx,
post={sender: Account(nonce=1), auth.authority: expected_authority},
expected_block_access_list=BlockAccessListExpectation(
account_expectations={
recipient: recipient_expectation,
auth.authority: authority_expectation,
}
),
)


def test_bal_7702_invalid_nonce_authorization(
pre: Alloc,
blockchain_test: BlockchainTestFiller,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
| `test_bal_7702_delegation_clear` | Ensure BAL captures clearing of EOA delegation | Alice first delegates to `Oracle`, then in second tx clears delegation by authorizing to `0x0` address. Each transaction sends 10 wei to Bob. Two variants: (1) Self-funded: Alice sends both 7702 txs herself. (2) Sponsored: `Relayer` sends both 7702 txs on Alice's behalf. | BAL **MUST** include Alice: first tx has `code_changes` (delegation designation `0xef0100\|\|address(Oracle)`), `nonce_changes`. Second tx has `code_changes` (empty code - delegation cleared), `nonce_changes`. Bob: `balance_changes` (receives 10 wei on each tx). For sponsored variant, BAL **MUST** also include `Relayer`: `nonce_changes` for both transactions. `Oracle` and `0x0` address **MUST NOT** be present in BAL - accounts are never accessed. | ✅ Completed |
| `test_bal_7702_delegated_storage_access` | Ensure BAL captures storage operations when calling a delegated EIP-7702 account | Alice has delegated her account to `Oracle`. `Oracle` contract contains code that reads from storage slot `0x01` and writes to storage slot `0x02`. Bob sends 10 wei to Alice (the delegated account), which executes `Oracle`'s code. | BAL **MUST** include Alice: `balance_changes` (receives 10 wei), `storage_changes` for slot `0x02` (write operation performed in Alice's storage), `storage_reads` for slot `0x01` (read operation from Alice's storage). Bob: `nonce_changes` (sender), `balance_changes` (loses 10 wei plus gas costs). `Oracle` (account access). | ✅ Completed |
| `test_bal_7702_top_frame_delegation_oog` | Ensure the delegation target of a delegated `tx.to` enters the BAL only when gas covers the top-frame delegation charge ([EIP-2780](https://eips.ethereum.org/EIPS/eip-2780) runtime charge) | `target` holds a pre-existing delegation to `delegated_to`. Sender sends a transaction to `target`. Parametrized: (1) `oog_at_delegation_charge`: gas limit is one short of intrinsic + top-frame delegation charge, (2) `success`: gas covers the charge and the delegated `STOP` runs. | For case (1): BAL **MUST** include `target` (recipient touch) but **MUST NOT** include `delegated_to` (the charge fails before the target is accessed). For case (2): BAL **MUST** include both `target` and `delegated_to` with empty changes. Sender always has `nonce_changes`. | ✅ Completed |
| `test_bal_7702_recipient_excluded_on_authorization_oog` | Ensure `tx.to` enters the BAL only when authorization processing completes ([EIP-2780](https://eips.ethereum.org/EIPS/eip-2780) runtime charges precede the recipient load) | Sender sends a transaction with one authorization (fresh authority) to a `STOP` contract. Parametrized: (1) `oog_at_authorization_charge`: gas limit is one short of the authorization's opening `NEW_ACCOUNT` charge, (2) `success`: gas covers the top-frame charges and the delegation applies. | For case (1): BAL **MUST NOT** include the recipient (the halt precedes its load) but **MUST** include the authority with empty changes (read during validation). For case (2): BAL **MUST** include the recipient with empty changes and the authority with `nonce_changes` and `code_changes`. | ✅ Completed |
| `test_bal_7702_invalid_nonce_authorization` | Ensure BAL handles failed authorization due to wrong nonce | `Relayer` sends sponsored transaction to Bob (10 wei transfer succeeds) but Alice's authorization to delegate to `Oracle` uses incorrect nonce, causing silent authorization failure | BAL **MUST** include Alice with empty changes (account access), Bob with `balance_changes` (receives 10 wei), Relayer with `nonce_changes`. **MUST NOT** include `Oracle` (authorization failed, no delegation) | ✅ Completed |
| `test_bal_7702_invalid_chain_id_authorization` | Ensure BAL handles failed authorization due to wrong chain id | `Relayer` sends sponsored transaction to Bob (10 wei transfer succeeds) but Alice's authorization to delegate to `Oracle` uses incorrect chain id, causing authorization failure before account access | BAL **MUST** include Bob with `balance_changes` (receives 10 wei), Relayer with `nonce_changes`. **MUST NOT** include Alice (authorization fails before loading account) or `Oracle` (authorization failed, no delegation) | ✅ Completed |
| `test_call_into_self_delegating_set_code` | Self-delegation degenerate one-hop case (companion to `test_call_into_chain_delegating_set_code`). File: `tests/prague/eip7702_set_code_tx/test_set_code_txs.py`. Parametrized over `@pytest.mark.with_all_call_opcodes`. | `auth_signer` auths itself as its own delegation target. `entry_address` issues `call_opcode(auth_signer)`. EVM resolves once: `auth_signer`'s code is the designator pointing back to `auth_signer`; the second hop is not followed, so the `0xef0100...` bytecode runs as legacy code → INVALID → returns 0. | `auth_signer` **MUST** appear with `nonce_changes` and `code_changes` (delegation designator to itself). `entry_address` **MUST** have `storage_reads=[0]` (no-op SSTORE demoted). No additional delegation-target entry is created because the target coincides with the authority. | ✅ Completed |
Expand Down
Loading