diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py new file mode 100644 index 00000000000..f06594fc79f --- /dev/null +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py @@ -0,0 +1,534 @@ +"""Tests for the effects of EIP-7702 transactions on EIP-7928.""" + +import pytest + +from ethereum_test_tools import ( + Account, + Alloc, + AuthorizationTuple, + Block, + BlockchainTestFiller, + Transaction, +) +from ethereum_test_types.block_access_list import ( + BalAccountExpectation, + BalBalanceChange, + BalCodeChange, + BalNonceChange, + BalStorageChange, + BalStorageSlot, + BlockAccessListExpectation, +) +from ethereum_test_vm import Opcodes as Op +from tests.prague.eip7702_set_code_tx.spec import Spec as Spec7702 + +from .spec import ref_spec_7928 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path +REFERENCE_SPEC_VERSION = ref_spec_7928.version + +pytestmark = pytest.mark.valid_from("Amsterdam") + + +@pytest.mark.parametrize( + "self_funded", + [ + pytest.param(False, id="sponsored"), + pytest.param(True, id="self_funded"), + ], +) +def test_bal_7702_delegation_create( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + self_funded: bool, +): + """Ensure BAL captures creation of EOA delegation.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + if not self_funded: + relayer = pre.fund_eoa() + sender = relayer + else: + sender = alice + + oracle = pre.deploy_contract(code=Op.STOP) + + tx = Transaction( + sender=sender, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle, + nonce=1 if self_funded else 0, + signer=alice, + ) + ], + ) + + account_expectations = { + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2 if self_funded else 1)], + code_changes=[ + BalCodeChange(tx_index=1, new_code=Spec7702.delegation_designation(oracle)) + ], + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)] + ), + # Oracle must not be present in BAL - the account is never accessed + oracle: None, + } + + # For sponsored variant, relayer must also be included in BAL + if not self_funded: + account_expectations[relayer] = BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + + post = { + alice: Account( + nonce=2 if self_funded else 1, code=Spec7702.delegation_designation(oracle) + ), + # Bob receives 10 wei + bob: Account(balance=10), + } + + # For sponsored variant, include relayer in post state + if not self_funded: + post.update({relayer: Account(nonce=1)}) + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + +@pytest.mark.parametrize( + "self_funded", + [ + pytest.param(False, id="sponsored"), + pytest.param(True, id="self_funded"), + ], +) +def test_bal_7702_delegation_update( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + self_funded: bool, +): + """Ensure BAL captures update of existing EOA delegation.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + if not self_funded: + relayer = pre.fund_eoa() + sender = relayer + else: + sender = alice + + oracle1 = pre.deploy_contract(code=Op.STOP) + oracle2 = pre.deploy_contract(code=Op.STOP) + + ## Perhaps create pre existing delegation, + ## see `test_bal_7702_delegated_storage_access` since + ## `test_bal_7702_delegation_create` already tests creation + tx_create = Transaction( + sender=sender, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle1, + nonce=1 if self_funded else 0, + signer=alice, + ) + ], + ) + + tx_update = Transaction( + nonce=2 if self_funded else 1, + sender=sender, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle2, + nonce=3 if self_funded else 1, + signer=alice, + ) + ], + ) + + account_expectations = { + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=2 if self_funded else 1), + BalNonceChange(tx_index=2, post_nonce=4 if self_funded else 2), + ], + code_changes=[ + BalCodeChange(tx_index=1, new_code=Spec7702.delegation_designation(oracle1)), + BalCodeChange(tx_index=2, new_code=Spec7702.delegation_designation(oracle2)), + ], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=10), + BalBalanceChange(tx_index=2, post_balance=20), + ] + ), + # Both delegation targets must not be present in BAL + # the account is never accessed + oracle1: None, + oracle2: None, + } + + # For sponsored variant, relayer must also be included in BAL + if not self_funded: + account_expectations[relayer] = BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1), + BalNonceChange(tx_index=2, post_nonce=2), + ], + ) + + block = Block( + txs=[tx_create, tx_update], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + + post = { + # Finally Alice's account should be delegated to oracle2 + alice: Account( + nonce=4 if self_funded else 2, code=Spec7702.delegation_designation(oracle2) + ), + # Bob receives 20 wei in total + bob: Account(balance=20), + } + + # For sponsored variant, include relayer in post state + if not self_funded: + post.update({relayer: Account(nonce=2)}) + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + +@pytest.mark.parametrize( + "self_funded", + [ + pytest.param(False, id="sponsored"), + pytest.param(True, id="self_funded"), + ], +) +def test_bal_7702_delegation_clear( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + self_funded: bool, +): + """Ensure BAL captures clearing of EOA delegation.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + if not self_funded: + relayer = pre.fund_eoa() + sender = relayer + else: + sender = alice + + oracle = pre.deploy_contract(code=Op.STOP) + abyss = Spec7702.RESET_DELEGATION_ADDRESS + + ## Perhaps create pre existing delegation, + ## see `test_bal_7702_delegated_storage_access` since + ## `test_bal_7702_delegation_create` already tests creation + tx_create = Transaction( + sender=sender, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle, + nonce=1 if self_funded else 0, + signer=alice, + ) + ], + ) + + tx_clear = Transaction( + nonce=2 if self_funded else 1, + sender=sender, + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=abyss, + nonce=3 if self_funded else 1, + signer=alice, + ) + ], + ) + + account_expectations = { + alice: BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=2 if self_funded else 1), + BalNonceChange(tx_index=2, post_nonce=4 if self_funded else 2), + ], + code_changes=[ + BalCodeChange(tx_index=1, new_code=Spec7702.delegation_designation(oracle)), + BalCodeChange(tx_index=2, new_code=""), + ], + ), + bob: BalAccountExpectation( + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=10), + BalBalanceChange(tx_index=2, post_balance=20), + ] + ), + # Both delegation targets must not be present in BAL + # the account is never accessed + oracle: None, + abyss: None, + } + + # For sponsored variant, relayer must also be included in BAL + if not self_funded: + account_expectations[relayer] = BalAccountExpectation( + nonce_changes=[ + BalNonceChange(tx_index=1, post_nonce=1), + BalNonceChange(tx_index=2, post_nonce=2), + ], + ) + + block = Block( + txs=[tx_create, tx_clear], + expected_block_access_list=BlockAccessListExpectation( + account_expectations=account_expectations + ), + ) + + post = { + # Finally Alice's account should NOT have any code + alice: Account(nonce=4 if self_funded else 2, code=""), + # Bob receives 20 wei in total + bob: Account(balance=20), + } + + # For sponsored variant, include relayer in post state + if not self_funded: + post.update({relayer: Account(nonce=2)}) + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + +def test_bal_7702_delegated_storage_access( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """ + Ensure BAL captures storage operations when calling a delegated + EIP-7702 account. + """ + # Oracle contract that reads from slot 0x01 and writes to slot 0x02 + oracle = pre.deploy_contract(code=Op.SLOAD(0x01) + Op.PUSH1(0x42) + Op.PUSH1(0x02) + Op.SSTORE) + bob = pre.fund_eoa() + + ## Is there a cleaner way to create pre-existing delegation? + alice = pre.deploy_contract(nonce=0x1, code=Spec7702.delegation_designation(oracle), balance=0) + + tx = Transaction( + sender=bob, + to=alice, # Bob calls Alice (delegated account) + value=10, + gas_limit=1_000_000, + gas_price=0xA, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)], + storage_changes=[ + BalStorageSlot( + slot=0x02, + slot_changes=[BalStorageChange(tx_index=1, post_value=0x42)], + ) + ], + storage_reads=[0x01], + ), + bob: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + # Oracle appears in BAL due to account access + # (delegation target) + oracle: BalAccountExpectation.empty(), + } + ), + ) + + post = { + alice: Account( + balance=10, + storage={0x02: 0x42}, + ), + bob: Account(nonce=1), + } + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + +def test_bal_7702_invalid_nonce_authorization( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """Ensure BAL handles failed authorization due to wrong nonce.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + relayer = pre.fund_eoa() + oracle = pre.deploy_contract(code=Op.STOP) + + tx = Transaction( + sender=relayer, # Sponsored transaction + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + address=oracle, + nonce=5, # Wrong nonce - Alice's actual nonce is 0 + signer=alice, + ) + ], + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + # No code_changes because authorization failed + nonce_changes=[], + code_changes=[], + ), + # Ensuring silent fail + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)] + ), + relayer: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + # Oracle must NOT be present - authorization failed so + # account is never accessed + oracle: None, + } + ), + ) + + post = { + relayer: Account(nonce=1), + bob: Account(balance=10), + } + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) + + +def test_bal_7702_invalid_chain_id_authorization( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """Ensure BAL handles failed authorization due to wrong chain id.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + relayer = pre.fund_eoa() + oracle = pre.deploy_contract(code=Op.STOP) + + tx = Transaction( + sender=relayer, # Sponsored transaction + to=bob, + value=10, + gas_limit=1_000_000, + gas_price=0xA, + authorization_list=[ + AuthorizationTuple( + chain_id=999, # Wrong chain id + address=oracle, + nonce=0, + signer=alice, + ) + ], + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + # Alice's account must not be read because + # authorization fails before loading her account + alice: None, + # Ensuring silent fail + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)] + ), + relayer: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + # Oracle must NOT be present - authorization failed so + # account never accessed + oracle: None, + } + ), + ) + + post = { + relayer: Account(nonce=1), + bob: Account(balance=10), + } + + blockchain_test( + # Set chain id here + # so this test holds if the default is + # ever changed + chain_id=1, + pre=pre, + blocks=[block], + post=post, + ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md index 033e37d5cab..a09c01dff2e 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -32,5 +32,11 @@ | `test_bal_precompile_funded_then_called` | BAL records precompile with balance change (fund) and access (call) | **Tx0**: Alice sends `1 ETH` to `ecrecover` (0x01). **Tx1**: Alice (or Bob) calls `ecrecover` with valid input and `0 ETH`. | BAL **MUST** include address `0x01` with `balance_changes` (from Tx0). No `storage_changes` or `code_changes`. | 🟡 Planned | | `test_bal_precompile_call_only` | BAL records precompile when called with no balance change | Alice calls `ecrecover` (0x01) with a valid input, sending **0 ETH**. | BAL **MUST** include address `0x01` in access list, with **no** `balance_changes`, `storage_changes`, or `code_changes`. | 🟡 Planned | | `test_bal_fully_unmutated_account` | Ensure BAL captures account that has zero net mutations | Alice sends 0 wei to `Oracle` which writes same pre-existing value to storage | BAL MUST include Alice with `nonce_changes` and balance changes (gas), `Oracle` with `storage_reads` for accessed slot but empty `storage_changes`. | ✅ Completed | +| `test_bal_7702_delegation_create` | Ensure BAL captures creation of EOA delegation | Alice authorizes delegation to contract `Oracle`. Transaction sends 10 wei to Bob. Two variants: (1) Self-funded: Alice sends 7702 tx herself. (2) Sponsored: `Relayer` sends 7702 tx on Alice's behalf. | BAL **MUST** include Alice: `code_changes` (delegation designation `0xef0100\|\|address(Oracle)`),`nonce_changes` (increment). Bob: `balance_changes` (receives 10 wei). For sponsored variant, BAL **MUST** also include `Relayer`:`nonce_changes`.`Oracle` **MUST NOT** be present in BAL - the account is never accessed. | ✅ Completed | +| `test_bal_7702_delegation_update` | Ensure BAL captures update of existing EOA delegation | Alice first delegates to `Oracle1`, then in second tx updates delegation to `Oracle2`. 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(Oracle1)`),`nonce_changes`. Second tx has`code_changes` (delegation designation `0xef0100\|\|address(Oracle2)`),`nonce_changes`. Bob:`balance_changes` (receives 10 wei on each tx). For sponsored variant, BAL **MUST** also include `Relayer`:`nonce_changes` for both transactions. `Oracle1` and `Oracle2` **MUST NOT** be present in BAL - accounts are never accessed. | ✅ Completed | +| `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_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 | > â„šī¸ Scope describes whether a test spans a single transaction (`tx`) or entire block (`blk`).