diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index 1f53ca1b211..b5e9e542142 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -31,8 +31,9 @@ REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path REFERENCE_SPEC_VERSION = ref_spec_7928.version +pytestmark = pytest.mark.valid_from("Amsterdam") + -@pytest.mark.valid_from("Amsterdam") def test_bal_nonce_changes( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -68,7 +69,6 @@ def test_bal_nonce_changes( ) -@pytest.mark.valid_from("Amsterdam") def test_bal_balance_changes( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -128,7 +128,6 @@ def test_bal_balance_changes( ) -@pytest.mark.valid_from("Amsterdam") def test_bal_storage_writes( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -174,7 +173,6 @@ def test_bal_storage_writes( ) -@pytest.mark.valid_from("Amsterdam") def test_bal_storage_reads( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -213,7 +211,6 @@ def test_bal_storage_reads( ) -@pytest.mark.valid_from("Amsterdam") def test_bal_code_changes( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -290,7 +287,6 @@ def test_bal_code_changes( ) -@pytest.mark.valid_from("Amsterdam") @pytest.mark.parametrize("self_destruct_in_same_tx", [True, False], ids=["same_tx", "new_tx"]) @pytest.mark.parametrize("pre_funded", [True, False], ids=["pre_funded", "not_pre_funded"]) def test_bal_self_destruct( @@ -424,3 +420,314 @@ def test_bal_self_destruct( blocks=[block], post=post, ) + + +@pytest.mark.parametrize( + "account_access_opcode", + [ + pytest.param(lambda target_addr: Op.BALANCE(target_addr), id="balance"), + pytest.param(lambda target_addr: Op.EXTCODESIZE(target_addr), id="extcodesize"), + pytest.param(lambda target_addr: Op.EXTCODECOPY(target_addr, 0, 0, 32), id="extcodecopy"), + pytest.param(lambda target_addr: Op.EXTCODEHASH(target_addr), id="extcodehash"), + pytest.param(lambda target_addr: Op.CALL(0, target_addr, 50, 0, 0, 0, 0), id="call"), + pytest.param( + lambda target_addr: Op.CALLCODE(0, target_addr, 0, 0, 0, 0, 0), id="callcode" + ), + pytest.param( + lambda target_addr: Op.DELEGATECALL(0, target_addr, 0, 0, 0, 0), id="delegatecall" + ), + pytest.param( + lambda target_addr: Op.STATICCALL(0, target_addr, 0, 0, 0, 0), id="staticcall" + ), + ], +) +def test_bal_account_access_target( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + account_access_opcode, +): + """Ensure BAL captures target address of account access opcodes.""" + alice = pre.fund_eoa() + target_contract = pre.deploy_contract(code=Op.STOP) + + oracle_contract = pre.deploy_contract( + balance=100, + code=account_access_opcode(target_contract), + ) + + tx = Transaction(sender=alice, to=oracle_contract, gas_limit=5_000_000, gas_price=0xA) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)] + ), + target_contract: BalAccountExpectation(), + oracle_contract: BalAccountExpectation(), + } + ), + ) + + blockchain_test(pre=pre, blocks=[block], post={}) + + +def test_bal_call_with_value_transfer( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """ + Ensure BAL captures balance changes from CALL opcode with + value transfer. + """ + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + # Oracle contract that uses CALL to transfer 100 wei to Bob + oracle_code = Op.CALL(0, bob, 100, 0, 0, 0, 0) + oracle_contract = pre.deploy_contract(code=oracle_code, balance=200) + + tx = Transaction(sender=alice, to=oracle_contract, gas_limit=1_000_000, gas_price=0xA) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + oracle_contract: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)], + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)], + ), + } + ), + ) + + blockchain_test(pre=pre, blocks=[block], post={}) + + +def test_bal_callcode_with_value_transfer( + pre: Alloc, + blockchain_test: BlockchainTestFiller, +): + """ + Ensure BAL captures balance changes from CALLCODE opcode with + value transfer. + """ + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + # TargetContract sends 100 wei to bob + target_code = Op.CALL(0, bob, 100, 0, 0, 0, 0) + target_contract = pre.deploy_contract(code=target_code) + + # Oracle contract that uses CALLCODE to execute TargetContract's code + oracle_code = Op.CALLCODE(50_000, target_contract, 100, 0, 0, 0, 0) + oracle_contract = pre.deploy_contract(code=oracle_code, balance=200) + + tx = Transaction(sender=alice, to=oracle_contract, gas_limit=1_000_000, gas_price=0xA) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + oracle_contract: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)], + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)], + ), + target_contract: BalAccountExpectation(), + } + ), + ) + + blockchain_test(pre=pre, blocks=[block], post={}) + + +@pytest.mark.parametrize( + "delegated_opcode", + [ + pytest.param( + lambda target_addr: Op.DELEGATECALL(50000, target_addr, 0, 0, 0, 0), id="delegatecall" + ), + pytest.param( + lambda target_addr: Op.CALLCODE(50000, target_addr, 0, 0, 0, 0, 0), id="callcode" + ), + ], +) +def test_bal_delegated_storage_writes( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + delegated_opcode, +): + """ + Ensure BAL captures delegated storage writes via + DELEGATECALL and CALLCODE. + """ + alice = pre.fund_eoa() + + # TargetContract that writes 0x42 to slot 0x01 + target_code = Op.SSTORE(0x01, 0x42) + target_contract = pre.deploy_contract(code=target_code) + + # Oracle contract that uses delegated opcode to execute + # TargetContract's code + oracle_code = delegated_opcode(target_contract) + oracle_contract = pre.deploy_contract(code=oracle_code) + + tx = Transaction( + sender=alice, + to=oracle_contract, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + oracle_contract: BalAccountExpectation( + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[BalStorageChange(tx_index=1, post_value=0x42)], + ) + ], + ), + target_contract: BalAccountExpectation(), + } + ), + ) + + blockchain_test(pre=pre, blocks=[block], post={}) + + +@pytest.mark.parametrize( + "delegated_opcode", + [ + pytest.param( + lambda target_addr: Op.DELEGATECALL(50000, target_addr, 0, 0, 0, 0), id="delegatecall" + ), + pytest.param( + lambda target_addr: Op.CALLCODE(50000, target_addr, 0, 0, 0, 0, 0), id="callcode" + ), + ], +) +def test_bal_delegated_storage_reads( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + delegated_opcode, +): + """ + Ensure BAL captures delegated storage reads via + DELEGATECALL and CALLCODE. + """ + alice = pre.fund_eoa() + + # TargetContract that reads from slot 0x01 + target_code = Op.SLOAD(0x01) + Op.STOP + target_contract = pre.deploy_contract(code=target_code) + + # Oracle contract with storage slot 0x01 = 0x42, + # uses delegated opcode to execute TargetContract's code + oracle_code = delegated_opcode(target_contract) + oracle_contract = pre.deploy_contract(code=oracle_code, storage={0x01: 0x42}) + + tx = Transaction( + sender=alice, + to=oracle_contract, + gas_limit=1_000_000, + ) + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + oracle_contract: BalAccountExpectation( + storage_reads=[0x01], + ), + target_contract: BalAccountExpectation(), + } + ), + ) + + blockchain_test(pre=pre, blocks=[block], post={}) + + +def test_bal_block_rewards( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + fork, +): + """Ensure BAL captures fee recipient balance changes from block rewards.""" + alice_initial_balance = 1_000_000 + alice = pre.fund_eoa(amount=alice_initial_balance) + bob = pre.fund_eoa(amount=0) + charlie = pre.fund_eoa(amount=0) # fee recipient + + intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() + intrinsic_gas_cost = intrinsic_gas_calculator( + calldata=b"", + contract_creation=False, + access_list=[], + ) + tx_gas_limit = intrinsic_gas_cost + 1000 # add a small buffer + gas_price = 0xA + base_fee_per_gas = 0x2 # Set base fee for EIP-1559 + + tx = Transaction( + sender=alice, + to=bob, + value=100, + gas_limit=tx_gas_limit, + gas_price=gas_price, + ) + + # EIP-1559 fee calculation: + # - Total gas cost + total_gas_cost = intrinsic_gas_cost * gas_price + # - Tip portion + tip_to_charlie = intrinsic_gas_cost * (gas_price - base_fee_per_gas) + + alice_final_balance = alice_initial_balance - 100 - total_gas_cost + + block = Block( + txs=[tx], + fee_recipient=charlie, # Set Charlie as the fee recipient + base_fee_per_gas=base_fee_per_gas, # Set base fee for EIP-1559 + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=alice_final_balance) + ], + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)], + ), + charlie: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=tip_to_charlie)], + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + 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 d5b3d2c11d9..a627b34cfaa 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -9,6 +9,13 @@ | `test_bal_code_changes` | Ensure BAL captures changes to account code | Alice deploys factory contract that creates new contract | BAL MUST include code changes for newly deployed contract | βœ… Completed | | `test_bal_self_destruct` | Ensure BAL captures storage access and balance changes caused by `SELFDESTRUCT` | Parameterized test: Alice interacts with a contract (either existing or created same-tx) that reads from storage slot 0x01, writes to storage slot 0x02, then executes `SELFDESTRUCT` with Bob as recipient. Contract may be pre-funded with 10 wei | BAL MUST include Alice's nonce change (increment) and Bob's balance change (100 or 110 depending on pre-funding). For the self-destructing contract: storage_reads=[0x01], empty storage_changes=[], and if pre-funded, balance_changes with post_balance=0; if not pre-funded, no balance change recorded. MUST NOT have code_changes or nonce_changes entries | βœ… Completed | | `test_bal_2930_slot_listed_but_untouched` | Ensure 2930 access list alone doesn't appear in BAL | Include `(KV, S=0x01)` in tx's EIP-2930 access list; tx executes code that does **no** `SLOAD`/`SSTORE` to `S` (e.g., pure arithmetic/log). | BAL **MUST NOT** contain any entry for `(KV, S)` β€” neither reads nor writes β€” because the slot wasn't touched. | 🟑 Planned | +| `test_bal_account_access_target` | Ensure BAL captures target addresses of account access opcodes | Alice calls `Oracle` contract which uses account access opcodes (`BALANCE`, `EXTCODESIZE`, `EXTCODECOPY`, `EXTCODEHASH`, `CALL`, `CALLCODE`, `DELEGATECALL`, `STATICCALL`) on `TargetContract`. | BAL MUST include Alice, `Oracle`, and `TargetContract` with empty changes for `TargetContract` and nonce changes for Alice. | βœ… Completed | +| `test_bal_call_with_value_transfer` | Ensure BAL captures balance changes from `CALL` opcode with value transfer | Alice calls `Oracle` contract (200 wei balance) which uses `CALL` opcode to transfer 100 wei to Bob (0 wei balance). | BAL MUST include Alice (nonce changes), Oracle (balance change to 100 wei), and Bob (balance change to 100 wei). | βœ… Completed | +| `test_bal_callcode_with_value_transfer` | Ensure BAL captures balance changes from `CALLCODE` opcode with value transfer | Alice calls `Oracle` contract (200 wei balance) which uses `CALLCODE` opcode to execute `TargetContract`'s code with 100 wei value transfer to Bob (0 wei balance). | BAL MUST include Alice (nonce changes), `Oracle` (balance change to 100 wei), Bob (balance change to 100 wei), and `TargetContract` (empty changes). | βœ… Completed | +| `test_bal_delegated_storage_writes` | Ensure BAL captures delegated storage writes via `DELEGATECALL` and `CALLCODE` | Alice calls `Oracle` contract which uses `DELEGATECALL`/`CALLCODE` to `TargetContract` that writes `0x42` to slot `0x01`. | BAL MUST include Alice (nonce changes), `Oracle` (storage changes for slot `0x01` = `0x42`), and `TargetContract` (empty changes). | βœ… Completed | +| `test_bal_delegated_storage_reads` | Ensure BAL captures delegated storage reads via `DELEGATECALL` and `CALLCODE` | Alice calls `Oracle` contract (with slot `0x01` = `0x42`) which uses `DELEGATECALL`/`CALLCODE` to `TargetContract` that reads from slot `0x01`. | BAL MUST include Alice (nonce changes), `Oracle` (storage reads for slot `0x01`), and `TargetContract` (empty changes). | βœ… Completed | +| `test_bal_block_rewards` | BAL tracks fee recipient balance changes from block rewards | Alice sends 100 wei to Bob with Charlie as fee recipient | BAL MUST include fee recipient Charlie with `balance_changes` reflecting transaction fees collected from the block. | βœ… Completed | +| `test_bal_2930_slot_listed_but_untouched` | Ensure 2930 access list alone doesn’t appear in BAL | Include `(KV, S=0x01)` in tx’s EIP-2930 access list; tx executes code that does **no** `SLOAD`/`SSTORE` to `S` (e.g., pure arithmetic/log). | BAL **MUST NOT** contain any entry for `(KV, S)` β€” neither reads nor writes β€” because the slot wasn’t touched. | 🟑 Planned | | `test_bal_2930_slot_listed_and_modified` | Ensure BAL records writes only because the slot is touched | Same access list as above, but tx executes `SSTORE` to `S`. | BAL **MUST** include `storage_changes` for `(KV, S)` (and no separate read record for that slot if implementation deduplicates). Presence in the access list is irrelevant; inclusion is due to the actual write. | 🟑 Planned | | `test_bal_7702_delegated_create` | BAL tracks EIP-7702 delegation indicator write and contract creation | Alice sends a type-4 (7702) tx authorizing herself to delegate to `Deployer` code which executes `CREATE` | BAL MUST include for **Alice**: `code_changes` (delegation indicator), `nonce_changes` (increment from 7702 processing), and `balance_changes` (post-gas). For **Child**: `code_changes` (runtime bytecode) and `nonce_changes = 1`. | 🟑 Planned | | `test_bal_self_transfer` | BAL handles self-transfers correctly | Alice sends `1 ETH` to **Alice** | BAL MUST include **one** entry for Alice with `balance_changes` reflecting **gas only** (value cancels out) and a nonce change; Coinbase balance updated for fees; no separate recipient row. | 🟑 Planned |