Skip to content

Commit 49cba58

Browse files
committed
fix(spec-specs, tests): exclude unloaded recipient from the BAL
(cherry picked from commit 89d15f3)
1 parent 55d4d1c commit 49cba58

4 files changed

Lines changed: 105 additions & 14 deletions

File tree

src/ethereum/forks/amsterdam/utils/message.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from ethereum.state import Address
1919

20-
from ..state_tracker import get_account, get_code
20+
from ..state_tracker import get_account
2121
from ..transactions import Transaction
2222
from ..vm import BlockEnvironment, Message, TransactionEnvironment
2323
from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS
@@ -63,9 +63,7 @@ def prepare_message(
6363
elif isinstance(tx.to, Address):
6464
current_target = tx.to
6565
msg_data = tx.data
66-
code = get_code(
67-
tx_env.state, get_account(tx_env.state, tx.to).code_hash
68-
)
66+
code = Bytes(b"")
6967
code_address = tx.to
7068
else:
7169
raise AssertionError("Target must be address or empty bytes")

tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_authorization_oog.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,10 @@ def test_set_delegation_oog_charge_point(
162162
state. The sender pays the full ``gas_limit`` and its nonce is not
163163
rolled back.
164164
165-
The recipient and both authorities were accessed before the halt
166-
(the recipient at inclusion, the authorities during validation), so
167-
per EIP-7928 all three must still appear in the block access list,
168-
with no recorded changes.
165+
Both authorities are read during authorization validation before
166+
the halt, so per EIP-7928 they still appear in the block access
167+
list with no recorded changes. The recipient is only loaded by the
168+
top-frame dispatch, which the halt precedes, so it must be absent.
169169
"""
170170
gas_costs = fork.gas_costs()
171171
sender_initial_balance = 10**18
@@ -230,10 +230,11 @@ def test_set_delegation_oog_charge_point(
230230
}
231231

232232
# An implementation recording accesses only for dispatched frames
233-
# would drop these entries and fork on the BAL hash.
233+
# would drop the authority entries; one recording the recipient at
234+
# inclusion would add it. Either forks on the BAL hash.
234235
expected_block_access_list = BlockAccessListExpectation(
235236
account_expectations={
236-
recipient: BalAccountExpectation.empty(),
237+
recipient: None,
237238
first.authority: BalAccountExpectation.empty(),
238239
second.authority: BalAccountExpectation.empty(),
239240
}
@@ -284,9 +285,9 @@ def test_set_delegation_oog_rolls_back_first_auth(
284285
The creation-first case is covered by
285286
``test_set_delegation_oog_charge_point[new_account]``.
286287
287-
The recipient and both authorities were accessed before the halt,
288-
so per EIP-7928 all three must still appear in the block access
289-
list, with no recorded changes.
288+
Both authorities are read during validation before the halt and
289+
stay in the block access list with no recorded changes; the
290+
recipient, never loaded before the halt, must be absent.
290291
"""
291292
gas_costs = fork.gas_costs()
292293
sender_initial_balance = 10**18
@@ -330,7 +331,7 @@ def test_set_delegation_oog_rolls_back_first_auth(
330331

331332
expected_block_access_list = BlockAccessListExpectation(
332333
account_expectations={
333-
recipient: BalAccountExpectation.empty(),
334+
recipient: None,
334335
first.authority: BalAccountExpectation.empty(),
335336
second.authority: BalAccountExpectation.empty(),
336337
}

tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
Initcode,
2323
Op,
2424
RecipientType,
25+
StateTestFiller,
2526
Transaction,
2627
Withdrawal,
2728
compute_create_address,
@@ -31,6 +32,10 @@
3132
)
3233

3334
from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702
35+
from ..eip2780_reduce_intrinsic_tx_gas.helpers import (
36+
AuthorizationAction,
37+
build_authorization,
38+
)
3439
from .spec import ref_spec_7928
3540

3641
REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path
@@ -547,6 +552,92 @@ def test_bal_7702_top_frame_delegation_oog(
547552
)
548553

549554

555+
@pytest.mark.parametrize(
556+
"outcome",
557+
[
558+
pytest.param("oog", id="oog_at_authorization_charge"),
559+
pytest.param("success", id="success"),
560+
],
561+
)
562+
def test_bal_7702_recipient_excluded_on_authorization_oog(
563+
fork: Fork,
564+
pre: Alloc,
565+
state_test: StateTestFiller,
566+
outcome: str,
567+
) -> None:
568+
"""
569+
Ensure ``tx.to`` enters the BAL only when authorization processing
570+
completes.
571+
572+
The single authorization is starved at its opening ``NEW_ACCOUNT``
573+
charge, halting the transaction before the top-frame dispatch loads
574+
the recipient: the recipient must be absent from the BAL, while the
575+
authority -- read during authorization validation -- stays in it
576+
with no recorded changes.
577+
"""
578+
sender = pre.fund_eoa()
579+
recipient = pre.deploy_contract(code=Op.STOP)
580+
581+
auth = build_authorization(pre, AuthorizationAction.CREATES_ACCOUNT)
582+
authorization_list = [auth.authorization]
583+
584+
intrinsic_regular = fork.transaction_intrinsic_cost_calculator()(
585+
recipient_type=RecipientType.CONTRACT,
586+
authorization_list_or_count=authorization_list,
587+
return_cost_deducted_prior_execution=True,
588+
)
589+
590+
recipient_expectation: BalAccountExpectation | None
591+
expected_authority: Account | None
592+
if outcome == "oog":
593+
# The authorization runs out at its opening NEW_ACCOUNT state
594+
# charge, drawn from gas_left under the zero state reservoir.
595+
gas_limit = intrinsic_regular + fork.gas_costs().NEW_ACCOUNT - 1
596+
recipient_expectation = None
597+
authority_expectation = BalAccountExpectation.empty()
598+
expected_authority = auth.original_account
599+
else:
600+
top_frame_regular = fork.transaction_top_frame_gas_calculator()(
601+
recipient_type=RecipientType.CONTRACT,
602+
authorizations=authorization_list,
603+
)
604+
top_frame_state = fork.transaction_top_frame_state_gas(
605+
recipient_type=RecipientType.CONTRACT,
606+
authorizations=authorization_list,
607+
)
608+
gas_limit = intrinsic_regular + top_frame_regular + top_frame_state
609+
recipient_expectation = BalAccountExpectation.empty()
610+
authority_expectation = BalAccountExpectation(
611+
nonce_changes=[BalNonceChange(block_access_index=1, post_nonce=1)],
612+
code_changes=[
613+
BalCodeChange(
614+
block_access_index=1,
615+
new_code=auth.applied_account.code,
616+
)
617+
],
618+
)
619+
expected_authority = auth.applied_account
620+
621+
tx = Transaction(
622+
sender=sender,
623+
to=recipient,
624+
authorization_list=authorization_list,
625+
gas_limit=gas_limit,
626+
)
627+
628+
state_test(
629+
pre=pre,
630+
tx=tx,
631+
post={sender: Account(nonce=1), auth.authority: expected_authority},
632+
expected_block_access_list=BlockAccessListExpectation(
633+
account_expectations={
634+
recipient: recipient_expectation,
635+
auth.authority: authority_expectation,
636+
}
637+
),
638+
)
639+
640+
550641
def test_bal_7702_invalid_nonce_authorization(
551642
pre: Alloc,
552643
blockchain_test: BlockchainTestFiller,

tests/amsterdam/eip7928_block_level_access_lists/test_cases.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
| `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 |
5252
| `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 |
5353
| `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 |
54+
| `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 |
5455
| `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 |
5556
| `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 |
5657
| `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 |

0 commit comments

Comments
 (0)