From 292ac3c9b10e5125b14a84be5612757614237159 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 16 Sep 2025 09:51:54 +0000 Subject: [PATCH 01/25] =?UTF-8?q?=E2=9C=A8=20feat(tests):=20EIP-7928=20SEL?= =?UTF-8?q?FDESTRUCT=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_block_access_lists.py | 34 +++++++++++++++++++ .../test_cases.md | 3 +- 2 files changed, 36 insertions(+), 1 deletion(-) 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 128f8276f8f..89804fafae8 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 @@ -284,3 +284,37 @@ def test_bal_code_changes( ), }, ) + + +@pytest.mark.valid_from("Amsterdam") +def test_bal_self_destruct(pre: Alloc, blockchain_test: BlockchainTestFiller): + """Ensure BAL captures balance changes caused by `SELFDESTRUCT`.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + kaboom = pre.deploy_contract(code=Op.SELFDESTRUCT(bob), balance=100) + + tx = Transaction(sender=alice, to=kaboom, 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)], + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)] + ), + kaboom: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)] + ), + } + ), + ) + + blockchain_test( + pre=pre, + blocks=[block], + post={alice: Account(nonce=1), kaboom: Account(balance=0), bob: Account(balance=100)}, + ) 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 3c371be939a..ea87d71d1f8 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -7,7 +7,8 @@ | `test_bal_storage_writes` | Ensure BAL captures storage writes | Alice calls contract that writes to storage slot `0x01` | BAL MUST include storage changes with correct slot and value | ✅ Completed | | `test_bal_storage_reads` | Ensure BAL captures storage reads | Alice calls contract that reads from storage slot `0x01` | BAL MUST include storage access for the read operation | ✅ Completed | | `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_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_self_destruct` | Ensure BAL captures balance changes caused by `SELFDESTRUCT` | Alice calls a contract (funded with 100 wei) that executes `SELFDESTRUCT` with Bob as its recipient | BAL MUST include Alice's nonce change (increment), the self-destructing contract's balance change (100 → 0), and Bob's balance change (0 → 100) | ✅ 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 | From 796eb7eb0143f7240560d88e17902998c1fdb24c Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 16 Sep 2025 10:04:48 +0000 Subject: [PATCH 02/25] =?UTF-8?q?=F0=9F=A5=A2=20nit:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_block_access_lists.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 89804fafae8..ea58c33f394 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 @@ -291,11 +291,9 @@ def test_bal_self_destruct(pre: Alloc, blockchain_test: BlockchainTestFiller): """Ensure BAL captures balance changes caused by `SELFDESTRUCT`.""" alice = pre.fund_eoa() bob = pre.fund_eoa(amount=0) - kaboom = pre.deploy_contract(code=Op.SELFDESTRUCT(bob), balance=100) tx = Transaction(sender=alice, to=kaboom, gas_limit=1_000_000) - block = Block( txs=[tx], expected_block_access_list=BlockAccessListExpectation( @@ -316,5 +314,9 @@ def test_bal_self_destruct(pre: Alloc, blockchain_test: BlockchainTestFiller): blockchain_test( pre=pre, blocks=[block], - post={alice: Account(nonce=1), kaboom: Account(balance=0), bob: Account(balance=100)}, + post={ + alice: Account(nonce=1), + bob: Account(balance=100), + kaboom: Account(balance=0), + }, ) From 548c4baaf12eece8f7f9d2eceddf14ec5259c81c Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 16 Sep 2025 13:08:28 +0000 Subject: [PATCH 03/25] =?UTF-8?q?=E2=9C=A8=20feat(tests):=20EIP-7928=20SEL?= =?UTF-8?q?FDESTRUCT=20Sendall?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_block_access_lists.py | 58 +++++++++++++++++-- .../test_cases.md | 2 +- 2 files changed, 53 insertions(+), 7 deletions(-) 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 ea58c33f394..d5b073a349d 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 @@ -287,13 +287,56 @@ def test_bal_code_changes( @pytest.mark.valid_from("Amsterdam") -def test_bal_self_destruct(pre: Alloc, blockchain_test: BlockchainTestFiller): +@pytest.mark.parametrize( + "self_destruct_in_same_tx", + [True, False], + ids=["self_destruct_in_same_tx", "self_destruct_in_a_new_tx"], +) +def test_bal_self_destruct( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + self_destruct_in_same_tx: bool, +): """Ensure BAL captures balance changes caused by `SELFDESTRUCT`.""" alice = pre.fund_eoa() bob = pre.fund_eoa(amount=0) - kaboom = pre.deploy_contract(code=Op.SELFDESTRUCT(bob), balance=100) - tx = Transaction(sender=alice, to=kaboom, gas_limit=1_000_000) + # A template, self-destructing contract + kaboom = pre.deploy_contract(code=Op.SELFDESTRUCT(bob)) + + if self_destruct_in_same_tx: + # The goal is to create a self-destructing contract in the same + # transaction to trigger deletion of code as per EIP-6780. + # The factory contract below clones the template `kaboom` + # contract and calls it in this transaction. + + template = pre[kaboom] + assert template is not None, "Template contract MUST be deployed for cloning" + + bytecode_size = len(template.code) + factory_bytecode = ( + # Clone template memory + Op.EXTCODECOPY(kaboom, 0, 0, bytecode_size) + # Fund 100 wei and deploy the clone + + Op.CREATE(100, 0, bytecode_size) + # Call the clone, which self-destructs + + Op.CALL(50_000, Op.DUP6, 0, 0, 0, 0, 0) + + Op.STOP + ) + + factory = pre.deploy_contract(code=factory_bytecode) + kaboom_same_tx = compute_create_address(address=factory, nonce=1) + + tx = Transaction( + sender=alice, + to=factory if self_destruct_in_same_tx else kaboom, + value=100, + gas_limit=1_000_000, + ) + + # Determine which account was destructed + self_destructed_account = kaboom_same_tx if self_destruct_in_same_tx else kaboom + block = Block( txs=[tx], expected_block_access_list=BlockAccessListExpectation( @@ -304,8 +347,12 @@ def test_bal_self_destruct(pre: Alloc, blockchain_test: BlockchainTestFiller): bob: BalAccountExpectation( balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)] ), - kaboom: BalAccountExpectation( - balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)] + self_destructed_account: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)], + # Expect code to be cleared if self-destructed in same transaction. + code_changes=[BalCodeChange(tx_index=1, new_code="")] + if self_destruct_in_same_tx + else [], ), } ), @@ -317,6 +364,5 @@ def test_bal_self_destruct(pre: Alloc, blockchain_test: BlockchainTestFiller): post={ alice: Account(nonce=1), bob: Account(balance=100), - kaboom: Account(balance=0), }, ) 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 ea87d71d1f8..57303214423 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -7,7 +7,7 @@ | `test_bal_storage_writes` | Ensure BAL captures storage writes | Alice calls contract that writes to storage slot `0x01` | BAL MUST include storage changes with correct slot and value | ✅ Completed | | `test_bal_storage_reads` | Ensure BAL captures storage reads | Alice calls contract that reads from storage slot `0x01` | BAL MUST include storage access for the read operation | ✅ Completed | | `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 balance changes caused by `SELFDESTRUCT` | Alice calls a contract (funded with 100 wei) that executes `SELFDESTRUCT` with Bob as its recipient | BAL MUST include Alice's nonce change (increment), the self-destructing contract's balance change (100 → 0), and Bob's balance change (0 → 100) | ✅ Completed | +| `test_bal_self_destruct` | Ensure BAL captures balance changes caused by `SELFDESTRUCT` | Alice calls a contract (funded with 100 wei) that executes `SELFDESTRUCT` with Bob as its recipient | BAL MUST include Alice's nonce change (increment), the self-destructing contract's balance change (100 → 0), and Bob's balance change (0 → 100). If the contract is created in the same transaction, BAL MUST also include code changes (new_code = empty bytes, indicating code has been cleared) | ✅ 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 | From a93ca2779721953e74085149339a21df208317d0 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 16 Sep 2025 13:19:49 +0000 Subject: [PATCH 04/25] =?UTF-8?q?=F0=9F=A5=A2=20nit:=20implicit=20post=20s?= =?UTF-8?q?tate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_block_access_lists.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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 d5b073a349d..c4c9e874439 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 @@ -358,11 +358,18 @@ def test_bal_self_destruct( ), ) + post = { + alice: Account(nonce=1), + bob: Account(balance=100), + } + + # If the account was NOT self-destructed in the same contract, + # we expect the account code to be present and its balance to be 0. + if not self_destruct_in_same_tx: + post[kaboom] = Account(balance=0, code=pre[kaboom].code) # type: ignore + blockchain_test( pre=pre, blocks=[block], - post={ - alice: Account(nonce=1), - bob: Account(balance=100), - }, + post=post, ) From a28abc3fd112336d308bb76e8a5ca606bd4991b0 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Fri, 19 Sep 2025 09:20:51 +0000 Subject: [PATCH 05/25] =?UTF-8?q?=F0=9F=A7=B9=20chore(tests):=20Move=20to?= =?UTF-8?q?=20new=20test=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_block_access_lists.py | 89 -------------- .../test_block_access_lists_self_destruct.py | 116 ++++++++++++++++++ 2 files changed, 116 insertions(+), 89 deletions(-) create mode 100644 tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py 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 c4c9e874439..128f8276f8f 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 @@ -284,92 +284,3 @@ def test_bal_code_changes( ), }, ) - - -@pytest.mark.valid_from("Amsterdam") -@pytest.mark.parametrize( - "self_destruct_in_same_tx", - [True, False], - ids=["self_destruct_in_same_tx", "self_destruct_in_a_new_tx"], -) -def test_bal_self_destruct( - pre: Alloc, - blockchain_test: BlockchainTestFiller, - self_destruct_in_same_tx: bool, -): - """Ensure BAL captures balance changes caused by `SELFDESTRUCT`.""" - alice = pre.fund_eoa() - bob = pre.fund_eoa(amount=0) - - # A template, self-destructing contract - kaboom = pre.deploy_contract(code=Op.SELFDESTRUCT(bob)) - - if self_destruct_in_same_tx: - # The goal is to create a self-destructing contract in the same - # transaction to trigger deletion of code as per EIP-6780. - # The factory contract below clones the template `kaboom` - # contract and calls it in this transaction. - - template = pre[kaboom] - assert template is not None, "Template contract MUST be deployed for cloning" - - bytecode_size = len(template.code) - factory_bytecode = ( - # Clone template memory - Op.EXTCODECOPY(kaboom, 0, 0, bytecode_size) - # Fund 100 wei and deploy the clone - + Op.CREATE(100, 0, bytecode_size) - # Call the clone, which self-destructs - + Op.CALL(50_000, Op.DUP6, 0, 0, 0, 0, 0) - + Op.STOP - ) - - factory = pre.deploy_contract(code=factory_bytecode) - kaboom_same_tx = compute_create_address(address=factory, nonce=1) - - tx = Transaction( - sender=alice, - to=factory if self_destruct_in_same_tx else kaboom, - value=100, - gas_limit=1_000_000, - ) - - # Determine which account was destructed - self_destructed_account = kaboom_same_tx if self_destruct_in_same_tx else kaboom - - block = Block( - txs=[tx], - expected_block_access_list=BlockAccessListExpectation( - account_expectations={ - alice: BalAccountExpectation( - nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], - ), - bob: BalAccountExpectation( - balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)] - ), - self_destructed_account: BalAccountExpectation( - balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)], - # Expect code to be cleared if self-destructed in same transaction. - code_changes=[BalCodeChange(tx_index=1, new_code="")] - if self_destruct_in_same_tx - else [], - ), - } - ), - ) - - post = { - alice: Account(nonce=1), - bob: Account(balance=100), - } - - # If the account was NOT self-destructed in the same contract, - # we expect the account code to be present and its balance to be 0. - if not self_destruct_in_same_tx: - post[kaboom] = Account(balance=0, code=pre[kaboom].code) # type: ignore - - blockchain_test( - pre=pre, - blocks=[block], - post=post, - ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py new file mode 100644 index 00000000000..65edcba6069 --- /dev/null +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py @@ -0,0 +1,116 @@ +"""Tests for effects of `SELFDESTRUCT` on EIP-7928 .""" + +import pytest + +from ethereum_test_tools import ( + Account, + Alloc, + Block, + BlockchainTestFiller, + Transaction, + compute_create_address, +) +from ethereum_test_tools.vm.opcode import Opcodes as Op +from ethereum_test_types.block_access_list import ( + BalAccountExpectation, + BalBalanceChange, + BalCodeChange, + BalNonceChange, + BlockAccessListExpectation, +) + +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_destruct_in_same_tx", + [True, False], + ids=["self_destruct_in_same_tx", "self_destruct_in_a_new_tx"], +) +def test_bal_self_destruct( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + self_destruct_in_same_tx: bool, +): + """Ensure BAL captures balance changes caused by `SELFDESTRUCT`.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + # A template, self-destructing contract + kaboom = pre.deploy_contract(code=Op.SELFDESTRUCT(bob)) + + if self_destruct_in_same_tx: + # The goal is to create a self-destructing contract in the same + # transaction to trigger deletion of code as per EIP-6780. + # The factory contract below clones the template `kaboom` + # contract and calls it in this transaction. + + template = pre[kaboom] + assert template is not None, "Template contract MUST be deployed for cloning" + + bytecode_size = len(template.code) + factory_bytecode = ( + # Clone template memory + Op.EXTCODECOPY(kaboom, 0, 0, bytecode_size) + # Fund 100 wei and deploy the clone + + Op.CREATE(100, 0, bytecode_size) + # Call the clone, which self-destructs + + Op.CALL(50_000, Op.DUP6, 0, 0, 0, 0, 0) + + Op.STOP + ) + + factory = pre.deploy_contract(code=factory_bytecode) + kaboom_same_tx = compute_create_address(address=factory, nonce=1) + + tx = Transaction( + sender=alice, + to=factory if self_destruct_in_same_tx else kaboom, + value=100, + gas_limit=1_000_000, + ) + + # Determine which account was destructed + self_destructed_account = kaboom_same_tx if self_destruct_in_same_tx else kaboom + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)] + ), + self_destructed_account: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)], + # Expect code to be cleared if self-destructed in same transaction. + code_changes=[BalCodeChange(tx_index=1, new_code="")] + if self_destruct_in_same_tx + else [], + ), + } + ), + ) + + post = { + alice: Account(nonce=1), + bob: Account(balance=100), + } + + # If the account was NOT self-destructed in the same contract, + # we expect the account code to be present and its balance to be 0. + if not self_destruct_in_same_tx: + post[kaboom] = Account(balance=0, code=pre[kaboom].code) # type: ignore + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) From effed86ba7764bf9115aaf9ae78f4e9210e76b9d Mon Sep 17 00:00:00 2001 From: fselmo Date: Mon, 22 Sep 2025 09:13:56 -0600 Subject: [PATCH 06/25] fix: Imports after rebase --- .../test_block_access_lists_self_destruct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py index 65edcba6069..b202678a59c 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py @@ -10,7 +10,6 @@ Transaction, compute_create_address, ) -from ethereum_test_tools.vm.opcode import Opcodes as Op from ethereum_test_types.block_access_list import ( BalAccountExpectation, BalBalanceChange, @@ -18,6 +17,7 @@ BalNonceChange, BlockAccessListExpectation, ) +from ethereum_test_vm import Opcodes as Op from .spec import ref_spec_7928 From 9cc5df17334ddd16fa62f4f629a37649a1c7dfde Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 23 Sep 2025 06:56:43 +0000 Subject: [PATCH 07/25] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20Abstract?= =?UTF-8?q?=20bytecode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_block_access_lists_self_destruct.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py index b202678a59c..1764a7ed41e 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py @@ -42,19 +42,17 @@ def test_bal_self_destruct( alice = pre.fund_eoa() bob = pre.fund_eoa(amount=0) - # A template, self-destructing contract - kaboom = pre.deploy_contract(code=Op.SELFDESTRUCT(bob)) + selfdestruct_code = Op.SELFDESTRUCT(bob) + # A pre existing self-destruct contract + kaboom = pre.deploy_contract(code=selfdestruct_code) if self_destruct_in_same_tx: # The goal is to create a self-destructing contract in the same # transaction to trigger deletion of code as per EIP-6780. - # The factory contract below clones the template `kaboom` + # The factory contract below creates a new self-destructing # contract and calls it in this transaction. - template = pre[kaboom] - assert template is not None, "Template contract MUST be deployed for cloning" - - bytecode_size = len(template.code) + bytecode_size = len(selfdestruct_code) factory_bytecode = ( # Clone template memory Op.EXTCODECOPY(kaboom, 0, 0, bytecode_size) From d156095ce8ba227f5e7eb3a3b12188f4f74b6fc4 Mon Sep 17 00:00:00 2001 From: raxhvl <10168946+raxhvl@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:47:08 +0200 Subject: [PATCH 08/25] nit Co-authored-by: felipe --- .../test_block_access_lists_self_destruct.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py index 1764a7ed41e..815ec0f4cf7 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py @@ -100,6 +100,7 @@ def test_bal_self_destruct( post = { alice: Account(nonce=1), bob: Account(balance=100), + kaboom: Account(balance=0, code=selfdestruct_code), } # If the account was NOT self-destructed in the same contract, From 88024d2bc158e8b34644b5106a986cf82b307159 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 23 Sep 2025 09:53:48 +0000 Subject: [PATCH 09/25] =?UTF-8?q?=F0=9F=90=9E=20fix:=20Post=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_block_access_lists_self_destruct.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py index 815ec0f4cf7..cc21b730590 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py @@ -103,10 +103,19 @@ def test_bal_self_destruct( kaboom: Account(balance=0, code=selfdestruct_code), } - # If the account was NOT self-destructed in the same contract, - # we expect the account code to be present and its balance to be 0. - if not self_destruct_in_same_tx: - post[kaboom] = Account(balance=0, code=pre[kaboom].code) # type: ignore + # If the account was self-destructed in the same transaction, + # we expect the account to non-existent and its balance to be 0. + if self_destruct_in_same_tx: + post.update( + { + factory: Account( + nonce=2, # incremented after CREATE + balance=0, # spent on CREATE + code=factory_bytecode, + ), + kaboom_same_tx: Account.NONEXISTENT, # type: ignore + } + ) blockchain_test( pre=pre, From 8a0f97e49921f1b9a4ebc153e6e51d0adafcb2be Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 23 Sep 2025 10:09:48 +0000 Subject: [PATCH 10/25] =?UTF-8?q?=F0=9F=90=9E=20fix:=20contract=20creation?= =?UTF-8?q?=20to=20use=20initcode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_block_access_lists_self_destruct.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py index cc21b730590..77625fa0106 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py @@ -5,15 +5,16 @@ from ethereum_test_tools import ( Account, Alloc, + BalCodeChange, Block, BlockchainTestFiller, + Initcode, Transaction, compute_create_address, ) from ethereum_test_types.block_access_list import ( BalAccountExpectation, BalBalanceChange, - BalCodeChange, BalNonceChange, BlockAccessListExpectation, ) @@ -46,16 +47,20 @@ def test_bal_self_destruct( # A pre existing self-destruct contract kaboom = pre.deploy_contract(code=selfdestruct_code) + # A template for self-destruct contract + self_destruct_init_code = Initcode(deploy_code=selfdestruct_code) + template = pre.deploy_contract(code=self_destruct_init_code) + if self_destruct_in_same_tx: # The goal is to create a self-destructing contract in the same # transaction to trigger deletion of code as per EIP-6780. # The factory contract below creates a new self-destructing # contract and calls it in this transaction. - bytecode_size = len(selfdestruct_code) + bytecode_size = len(self_destruct_init_code) factory_bytecode = ( # Clone template memory - Op.EXTCODECOPY(kaboom, 0, 0, bytecode_size) + Op.EXTCODECOPY(template, 0, 0, bytecode_size) # Fund 100 wei and deploy the clone + Op.CREATE(100, 0, bytecode_size) # Call the clone, which self-destructs From da8d56812009335fa81baaf01fb1c54aa2cee6c0 Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 23 Sep 2025 09:56:07 -0600 Subject: [PATCH 11/25] feat: point to latest commit in BALs specs --- .github/configs/eels_resolutions.json | 2 +- src/pytest_plugins/eels_resolutions.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/configs/eels_resolutions.json b/.github/configs/eels_resolutions.json index 44833d9a06a..528ef3395ec 100644 --- a/.github/configs/eels_resolutions.json +++ b/.github/configs/eels_resolutions.json @@ -52,6 +52,6 @@ "Amsterdam": { "git_url": "https://github.com/fselmo/execution-specs.git", "branch": "feat/amsterdam-fork-and-block-access-lists", - "commit": "39e0b59613be4100d2efc86702ff594c54e5bd81" + "commit": "8c82882175db17a81a3ee86e1827df3c51480337" } } diff --git a/src/pytest_plugins/eels_resolutions.json b/src/pytest_plugins/eels_resolutions.json index 2b1a9c5f9db..6fabfe399f1 100644 --- a/src/pytest_plugins/eels_resolutions.json +++ b/src/pytest_plugins/eels_resolutions.json @@ -55,6 +55,6 @@ "Amsterdam": { "git_url": "https://github.com/fselmo/execution-specs.git", "branch": "feat/amsterdam-fork-and-block-access-lists", - "commit": "39e0b59613be4100d2efc86702ff594c54e5bd81" + "commit": "8c82882175db17a81a3ee86e1827df3c51480337" } } From 1250227b835fa75dffce14af0fef8cb16737ed3b Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 23 Sep 2025 16:57:21 +0000 Subject: [PATCH 12/25] =?UTF-8?q?=E2=9C=A8=20feat:=20update=20nonce=20chec?= =?UTF-8?q?k?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_block_access_lists_self_destruct.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py index 77625fa0106..b417101f8b6 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py @@ -92,6 +92,11 @@ def test_bal_self_destruct( balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)] ), self_destructed_account: BalAccountExpectation( + # When an account is deleted its post nonce is set to 0 + # as per EIP-7928. + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=0)] + if self_destruct_in_same_tx + else [], balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)], # Expect code to be cleared if self-destructed in same transaction. code_changes=[BalCodeChange(tx_index=1, new_code="")] From 80ec49455632422fa2ebf464e495f0894181468b Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 23 Sep 2025 17:34:07 +0000 Subject: [PATCH 13/25] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20move=20t?= =?UTF-8?q?o=20test=5Fblock=5Faccess=5Flists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_block_access_lists.py | 107 ++++++++++++++ .../test_block_access_lists_self_destruct.py | 134 ------------------ .../test_cases.md | 2 +- 3 files changed, 108 insertions(+), 135 deletions(-) delete mode 100644 tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py 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 128f8276f8f..d6cbc627cdf 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 @@ -7,6 +7,7 @@ Alloc, Block, BlockchainTestFiller, + Initcode, Storage, Transaction, compute_create_address, @@ -284,3 +285,109 @@ def test_bal_code_changes( ), }, ) + + +@pytest.mark.valid_from("Amsterdam") +@pytest.mark.parametrize( + "self_destruct_in_same_tx", + [True, False], + ids=["self_destruct_in_same_tx", "self_destruct_in_a_new_tx"], +) +def test_bal_self_destruct( + pre: Alloc, + blockchain_test: BlockchainTestFiller, + self_destruct_in_same_tx: bool, +): + """Ensure BAL captures balance changes caused by `SELFDESTRUCT`.""" + alice = pre.fund_eoa() + bob = pre.fund_eoa(amount=0) + + selfdestruct_code = Op.SELFDESTRUCT(bob) + # A pre existing self-destruct contract + kaboom = pre.deploy_contract(code=selfdestruct_code) + + # A template for self-destruct contract + self_destruct_init_code = Initcode(deploy_code=selfdestruct_code) + template = pre.deploy_contract(code=self_destruct_init_code) + + if self_destruct_in_same_tx: + # The goal is to create a self-destructing contract in the same + # transaction to trigger deletion of code as per EIP-6780. + # The factory contract below creates a new self-destructing + # contract and calls it in this transaction. + + bytecode_size = len(self_destruct_init_code) + factory_bytecode = ( + # Clone template memory + Op.EXTCODECOPY(template, 0, 0, bytecode_size) + # Fund 100 wei and deploy the clone + + Op.CREATE(100, 0, bytecode_size) + # Call the clone, which self-destructs + + Op.CALL(50_000, Op.DUP6, 0, 0, 0, 0, 0) + + Op.STOP + ) + + factory = pre.deploy_contract(code=factory_bytecode) + kaboom_same_tx = compute_create_address(address=factory, nonce=1) + + tx = Transaction( + sender=alice, + to=factory if self_destruct_in_same_tx else kaboom, + value=100, + gas_limit=1_000_000, + ) + + # Determine which account was destructed + self_destructed_account = kaboom_same_tx if self_destruct_in_same_tx else kaboom + + block = Block( + txs=[tx], + expected_block_access_list=BlockAccessListExpectation( + account_expectations={ + alice: BalAccountExpectation( + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], + ), + bob: BalAccountExpectation( + balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)] + ), + self_destructed_account: BalAccountExpectation( + # When an account is deleted its post nonce is set to 0 + # as per EIP-7928. + nonce_changes=[BalNonceChange(tx_index=1, post_nonce=0)] + if self_destruct_in_same_tx + else [], + balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)], + # Expect code to be cleared if self-destructed in same transaction. + code_changes=[BalCodeChange(tx_index=1, new_code="")] + if self_destruct_in_same_tx + else [], + ), + } + ), + ) + + post = { + alice: Account(nonce=1), + bob: Account(balance=100), + kaboom: Account(balance=0, code=selfdestruct_code), + } + + # If the account was self-destructed in the same transaction, + # we expect the account to non-existent and its balance to be 0. + if self_destruct_in_same_tx: + post.update( + { + factory: Account( + nonce=2, # incremented after CREATE + balance=0, # spent on CREATE + code=factory_bytecode, + ), + kaboom_same_tx: Account.NONEXISTENT, # type: ignore + } + ) + + blockchain_test( + pre=pre, + blocks=[block], + post=post, + ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py deleted file mode 100644 index b417101f8b6..00000000000 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_self_destruct.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Tests for effects of `SELFDESTRUCT` on EIP-7928 .""" - -import pytest - -from ethereum_test_tools import ( - Account, - Alloc, - BalCodeChange, - Block, - BlockchainTestFiller, - Initcode, - Transaction, - compute_create_address, -) -from ethereum_test_types.block_access_list import ( - BalAccountExpectation, - BalBalanceChange, - BalNonceChange, - BlockAccessListExpectation, -) -from ethereum_test_vm import Opcodes as Op - -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_destruct_in_same_tx", - [True, False], - ids=["self_destruct_in_same_tx", "self_destruct_in_a_new_tx"], -) -def test_bal_self_destruct( - pre: Alloc, - blockchain_test: BlockchainTestFiller, - self_destruct_in_same_tx: bool, -): - """Ensure BAL captures balance changes caused by `SELFDESTRUCT`.""" - alice = pre.fund_eoa() - bob = pre.fund_eoa(amount=0) - - selfdestruct_code = Op.SELFDESTRUCT(bob) - # A pre existing self-destruct contract - kaboom = pre.deploy_contract(code=selfdestruct_code) - - # A template for self-destruct contract - self_destruct_init_code = Initcode(deploy_code=selfdestruct_code) - template = pre.deploy_contract(code=self_destruct_init_code) - - if self_destruct_in_same_tx: - # The goal is to create a self-destructing contract in the same - # transaction to trigger deletion of code as per EIP-6780. - # The factory contract below creates a new self-destructing - # contract and calls it in this transaction. - - bytecode_size = len(self_destruct_init_code) - factory_bytecode = ( - # Clone template memory - Op.EXTCODECOPY(template, 0, 0, bytecode_size) - # Fund 100 wei and deploy the clone - + Op.CREATE(100, 0, bytecode_size) - # Call the clone, which self-destructs - + Op.CALL(50_000, Op.DUP6, 0, 0, 0, 0, 0) - + Op.STOP - ) - - factory = pre.deploy_contract(code=factory_bytecode) - kaboom_same_tx = compute_create_address(address=factory, nonce=1) - - tx = Transaction( - sender=alice, - to=factory if self_destruct_in_same_tx else kaboom, - value=100, - gas_limit=1_000_000, - ) - - # Determine which account was destructed - self_destructed_account = kaboom_same_tx if self_destruct_in_same_tx else kaboom - - block = Block( - txs=[tx], - expected_block_access_list=BlockAccessListExpectation( - account_expectations={ - alice: BalAccountExpectation( - nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], - ), - bob: BalAccountExpectation( - balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)] - ), - self_destructed_account: BalAccountExpectation( - # When an account is deleted its post nonce is set to 0 - # as per EIP-7928. - nonce_changes=[BalNonceChange(tx_index=1, post_nonce=0)] - if self_destruct_in_same_tx - else [], - balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)], - # Expect code to be cleared if self-destructed in same transaction. - code_changes=[BalCodeChange(tx_index=1, new_code="")] - if self_destruct_in_same_tx - else [], - ), - } - ), - ) - - post = { - alice: Account(nonce=1), - bob: Account(balance=100), - kaboom: Account(balance=0, code=selfdestruct_code), - } - - # If the account was self-destructed in the same transaction, - # we expect the account to non-existent and its balance to be 0. - if self_destruct_in_same_tx: - post.update( - { - factory: Account( - nonce=2, # incremented after CREATE - balance=0, # spent on CREATE - code=factory_bytecode, - ), - kaboom_same_tx: Account.NONEXISTENT, # type: ignore - } - ) - - blockchain_test( - 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 57303214423..7470bd1731b 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -7,7 +7,7 @@ | `test_bal_storage_writes` | Ensure BAL captures storage writes | Alice calls contract that writes to storage slot `0x01` | BAL MUST include storage changes with correct slot and value | ✅ Completed | | `test_bal_storage_reads` | Ensure BAL captures storage reads | Alice calls contract that reads from storage slot `0x01` | BAL MUST include storage access for the read operation | ✅ Completed | | `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 balance changes caused by `SELFDESTRUCT` | Alice calls a contract (funded with 100 wei) that executes `SELFDESTRUCT` with Bob as its recipient | BAL MUST include Alice's nonce change (increment), the self-destructing contract's balance change (100 → 0), and Bob's balance change (0 → 100). If the contract is created in the same transaction, BAL MUST also include code changes (new_code = empty bytes, indicating code has been cleared) | ✅ Completed | +| `test_bal_self_destruct` | Ensure BAL captures balance changes caused by `SELFDESTRUCT` | Alice calls a contract (funded with 100 wei) that executes `SELFDESTRUCT` with Bob as its recipient | BAL MUST include Alice's nonce change (increment), the self-destructing contract's balance change (100 → 0), and Bob's balance change (0 → 100). The self-destructing contract MUST also appear with `post_nonce=0`, `post_balance=0`, and `new_code=0x`, indicating an account deletion | ✅ 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 | From 656df691d8c769adf59c2c8833cf7a8e983ab778 Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 23 Sep 2025 11:34:47 -0600 Subject: [PATCH 14/25] feat: Validate t8n BAL does not have duplicate entries for the same tx_index --- .../block_access_list/expectations.py | 71 +++++++++++------- .../tests/test_block_access_lists.py | 72 +++++++++++++++++++ 2 files changed, 118 insertions(+), 25 deletions(-) diff --git a/src/ethereum_test_types/block_access_list/expectations.py b/src/ethereum_test_types/block_access_list/expectations.py index 8bcdeba5381..f60b3c2ea76 100644 --- a/src/ethereum_test_types/block_access_list/expectations.py +++ b/src/ethereum_test_types/block_access_list/expectations.py @@ -199,21 +199,33 @@ def _validate_bal_ordering(bal: "BlockAccessList") -> None: f"{bal.root[i - 1].address} >= {bal.root[i].address}" ) - # Check transaction index ordering within accounts + # Check transaction index ordering and uniqueness within accounts for account in bal.root: - change_lists: List[BlockAccessListChangeLists] = [ - account.nonce_changes, - account.balance_changes, - account.code_changes, + changes_to_check: List[tuple[str, BlockAccessListChangeLists]] = [ + ("nonce_changes", account.nonce_changes), + ("balance_changes", account.balance_changes), + ("code_changes", account.code_changes), ] - for change_list in change_lists: - for i in range(1, len(change_list)): - if change_list[i - 1].tx_index >= change_list[i].tx_index: - raise BlockAccessListValidationError( - f"Transaction indices not in ascending order in account " - f"{account.address}: {change_list[i - 1].tx_index} >= " - f"{change_list[i].tx_index}" - ) + + for field_name, change_list in changes_to_check: + if not change_list: + continue + + tx_indices = [c.tx_index for c in change_list] + + # Check both ordering and duplicates + if tx_indices != sorted(tx_indices): + raise BlockAccessListValidationError( + f"Transaction indices not in ascending order in {field_name} of account " + f"{account.address}. Got: {tx_indices}, Expected: {sorted(tx_indices)}" + ) + + if len(tx_indices) != len(set(tx_indices)): + duplicates = sorted({idx for idx in tx_indices if tx_indices.count(idx) > 1}) + raise BlockAccessListValidationError( + f"Duplicate transaction indices in {field_name} of account " + f"{account.address}. Duplicates: {duplicates}" + ) # Check storage slot ordering for i in range(1, len(account.storage_changes)): @@ -224,19 +236,28 @@ def _validate_bal_ordering(bal: "BlockAccessList") -> None: f"{account.storage_changes[i].slot}" ) - # Check transaction index ordering within storage slots + # Check transaction index ordering and uniqueness within storage slots for storage_slot in account.storage_changes: - for i in range(1, len(storage_slot.slot_changes)): - if ( - storage_slot.slot_changes[i - 1].tx_index - >= storage_slot.slot_changes[i].tx_index - ): - raise BlockAccessListValidationError( - f"Transaction indices not in ascending order in storage slot " - f"{storage_slot.slot} of account {account.address}: " - f"{storage_slot.slot_changes[i - 1].tx_index} >= " - f"{storage_slot.slot_changes[i].tx_index}" - ) + if not storage_slot.slot_changes: + continue + + tx_indices = [c.tx_index for c in storage_slot.slot_changes] + + # Check both ordering and duplicates + if tx_indices != sorted(tx_indices): + raise BlockAccessListValidationError( + f"Transaction indices not in ascending order in storage slot " + f"{storage_slot.slot} of account {account.address}. " + f"Got: {tx_indices}, Expected: {sorted(tx_indices)}" + ) + + if len(tx_indices) != len(set(tx_indices)): + duplicates = sorted({idx for idx in tx_indices if tx_indices.count(idx) > 1}) + raise BlockAccessListValidationError( + f"Duplicate transaction indices in storage slot " + f"{storage_slot.slot} of account {account.address}. " + f"Duplicates: {duplicates}" + ) # Check storage reads ordering for i in range(1, len(account.storage_reads)): diff --git a/src/ethereum_test_types/tests/test_block_access_lists.py b/src/ethereum_test_types/tests/test_block_access_lists.py index 06650810722..c4d9940833b 100644 --- a/src/ethereum_test_types/tests/test_block_access_lists.py +++ b/src/ethereum_test_types/tests/test_block_access_lists.py @@ -335,6 +335,78 @@ def test_actual_bal_tx_indices_ordering(field_name): expectation.verify_against(actual_bal) +@pytest.mark.parametrize( + "field_name", + ["nonce_changes", "balance_changes", "code_changes"], +) +def test_actual_bal_duplicate_tx_indices(field_name): + """Test that actual BAL must not have duplicate tx indices in change lists.""" + addr = Address(0xA) + + # Duplicate tx_index=1 + changes = [] + if field_name == "nonce_changes": + changes = [ + BalNonceChange(tx_index=1, post_nonce=1), + BalNonceChange(tx_index=1, post_nonce=2), # duplicate tx_index + BalNonceChange(tx_index=2, post_nonce=3), + ] + elif field_name == "balance_changes": + changes = [ + BalBalanceChange(tx_index=1, post_balance=100), + BalBalanceChange(tx_index=1, post_balance=200), # duplicate tx_index + BalBalanceChange(tx_index=2, post_balance=300), + ] + elif field_name == "code_changes": + changes = [ + BalCodeChange(tx_index=1, new_code=b"code1"), + BalCodeChange(tx_index=1, new_code=b""), # duplicate tx_index + BalCodeChange(tx_index=2, new_code=b"code2"), + ] + + actual_bal = BlockAccessList([BalAccountChange(address=addr, **{field_name: changes})]) + + expectation = BlockAccessListExpectation(account_expectations={}) + + with pytest.raises( + BlockAccessListValidationError, + match=f"Duplicate transaction indices in {field_name}.*Duplicates: \\[1\\]", + ): + expectation.verify_against(actual_bal) + + +def test_actual_bal_storage_duplicate_tx_indices(): + """Test that storage changes must not have duplicate tx indices within same slot.""" + addr = Address(0xA) + + # Create storage changes with duplicate tx_index within the same slot + actual_bal = BlockAccessList( + [ + BalAccountChange( + address=addr, + storage_changes=[ + BalStorageSlot( + slot=0x01, + slot_changes=[ + BalStorageChange(tx_index=1, post_value=0x100), + BalStorageChange(tx_index=1, post_value=0x200), # duplicate tx_index + BalStorageChange(tx_index=2, post_value=0x300), + ], + ) + ], + ) + ] + ) + + expectation = BlockAccessListExpectation(account_expectations={}) + + with pytest.raises( + BlockAccessListValidationError, + match="Duplicate transaction indices in storage slot.*Duplicates: \\[1\\]", + ): + expectation.verify_against(actual_bal) + + def test_expected_addresses_auto_sorted(): """ Test that expected addresses are automatically sorted before comparison. From 27599f78e54770f921636acb82b84ce47e9249c0 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 23 Sep 2025 18:15:57 +0000 Subject: [PATCH 15/25] =?UTF-8?q?=F0=9F=93=84=20docs:=20Changelog=20entry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 032fe1bec6c..42654c78583 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -29,6 +29,7 @@ Test fixtures for use by clients are available for each release on the [Github r - ✨ Add flexible API for absence checks for EIP-7928 (BAL) tests ([#2124](https://github.com/ethereum/execution-spec-tests/pull/2124)). - 🐞 Use ``engine_newPayloadV5`` for `>=Amsterdam` forks in `consume engine` ([#2170](https://github.com/ethereum/execution-spec-tests/pull/2170)). - 🔀 Refactor EIP-7928 (BAL) absence checks into a friendlier class-based DevEx ([#2175](https://github.com/ethereum/execution-spec-tests/pull/2175)). +- ✨ Add an EIP-7928 test case targeting `SELFDESTRUCT` opcode. ([#2159](https://github.com/ethereum/execution-spec-tests/pull/2159)). ### 🧪 Test Cases From a15735c548e25422692b50bb12535390b0b0f0cd Mon Sep 17 00:00:00 2001 From: raxhvl Date: Tue, 23 Sep 2025 18:43:24 +0000 Subject: [PATCH 16/25] =?UTF-8?q?=F0=9F=93=84=20docs:=20Changelog=20entry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 42654c78583..aecab2d77f7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -29,10 +29,11 @@ Test fixtures for use by clients are available for each release on the [Github r - ✨ Add flexible API for absence checks for EIP-7928 (BAL) tests ([#2124](https://github.com/ethereum/execution-spec-tests/pull/2124)). - 🐞 Use ``engine_newPayloadV5`` for `>=Amsterdam` forks in `consume engine` ([#2170](https://github.com/ethereum/execution-spec-tests/pull/2170)). - 🔀 Refactor EIP-7928 (BAL) absence checks into a friendlier class-based DevEx ([#2175](https://github.com/ethereum/execution-spec-tests/pull/2175)). -- ✨ Add an EIP-7928 test case targeting `SELFDESTRUCT` opcode. ([#2159](https://github.com/ethereum/execution-spec-tests/pull/2159)). ### 🧪 Test Cases +- ✨ Add an EIP-7928 test case targeting the `SELFDESTRUCT` opcode. ([#2159](https://github.com/ethereum/execution-spec-tests/pull/2159)). + ## [v5.0.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v5.0.0) - 2025-09-05 ## 🇯🇵 Summary From 44b2d4bbea71cc72af4985ed0f9df4a61b173fa1 Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 24 Sep 2025 07:34:26 -0600 Subject: [PATCH 17/25] chore: point to latest specs commit --- .github/configs/eels_resolutions.json | 2 +- src/pytest_plugins/eels_resolutions.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/configs/eels_resolutions.json b/.github/configs/eels_resolutions.json index 528ef3395ec..9177db60bd6 100644 --- a/.github/configs/eels_resolutions.json +++ b/.github/configs/eels_resolutions.json @@ -52,6 +52,6 @@ "Amsterdam": { "git_url": "https://github.com/fselmo/execution-specs.git", "branch": "feat/amsterdam-fork-and-block-access-lists", - "commit": "8c82882175db17a81a3ee86e1827df3c51480337" + "commit": "08b46ef83300da7a47f9ee18884620e1700259e8" } } diff --git a/src/pytest_plugins/eels_resolutions.json b/src/pytest_plugins/eels_resolutions.json index 6fabfe399f1..2f783714338 100644 --- a/src/pytest_plugins/eels_resolutions.json +++ b/src/pytest_plugins/eels_resolutions.json @@ -55,6 +55,6 @@ "Amsterdam": { "git_url": "https://github.com/fselmo/execution-specs.git", "branch": "feat/amsterdam-fork-and-block-access-lists", - "commit": "8c82882175db17a81a3ee86e1827df3c51480337" + "commit": "08b46ef83300da7a47f9ee18884620e1700259e8" } } From c4fb252c48469081e4efa0c56be0948e700b2ed0 Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 24 Sep 2025 07:41:09 -0600 Subject: [PATCH 18/25] chore: avoid extra fields in BAL classes, related to #2197 --- .../block_access_list/account_absent_values.py | 2 ++ .../block_access_list/account_changes.py | 12 ++++++++++++ .../block_access_list/expectations.py | 2 ++ 3 files changed, 16 insertions(+) diff --git a/src/ethereum_test_types/block_access_list/account_absent_values.py b/src/ethereum_test_types/block_access_list/account_absent_values.py index c0fea606a97..d2109477cbc 100644 --- a/src/ethereum_test_types/block_access_list/account_absent_values.py +++ b/src/ethereum_test_types/block_access_list/account_absent_values.py @@ -74,6 +74,8 @@ class BalAccountAbsentValues(CamelModel): """ + model_config = CamelModel.model_config | {"extra": "forbid"} + nonce_changes: List[BalNonceChange] = Field( default_factory=list, description="List of nonce changes that should NOT exist in the BAL. " diff --git a/src/ethereum_test_types/block_access_list/account_changes.py b/src/ethereum_test_types/block_access_list/account_changes.py index 5edb0b9fc5e..b5a9632c8ab 100644 --- a/src/ethereum_test_types/block_access_list/account_changes.py +++ b/src/ethereum_test_types/block_access_list/account_changes.py @@ -22,6 +22,8 @@ class BalNonceChange(CamelModel, RLPSerializable): """Represents a nonce change in the block access list.""" + model_config = CamelModel.model_config | {"extra": "forbid"} + tx_index: HexNumber = Field( HexNumber(1), description="Transaction index where the change occurred", @@ -34,6 +36,8 @@ class BalNonceChange(CamelModel, RLPSerializable): class BalBalanceChange(CamelModel, RLPSerializable): """Represents a balance change in the block access list.""" + model_config = CamelModel.model_config | {"extra": "forbid"} + tx_index: HexNumber = Field( HexNumber(1), description="Transaction index where the change occurred", @@ -46,6 +50,8 @@ class BalBalanceChange(CamelModel, RLPSerializable): class BalCodeChange(CamelModel, RLPSerializable): """Represents a code change in the block access list.""" + model_config = CamelModel.model_config | {"extra": "forbid"} + tx_index: HexNumber = Field( HexNumber(1), description="Transaction index where the change occurred", @@ -58,6 +64,8 @@ class BalCodeChange(CamelModel, RLPSerializable): class BalStorageChange(CamelModel, RLPSerializable): """Represents a change to a specific storage slot.""" + model_config = CamelModel.model_config | {"extra": "forbid"} + tx_index: HexNumber = Field( HexNumber(1), description="Transaction index where the change occurred", @@ -70,6 +78,8 @@ class BalStorageChange(CamelModel, RLPSerializable): class BalStorageSlot(CamelModel, RLPSerializable): """Represents all changes to a specific storage slot.""" + model_config = CamelModel.model_config | {"extra": "forbid"} + slot: StorageKey = Field(..., description="Storage slot key") slot_changes: List[BalStorageChange] = Field( default_factory=list, description="List of changes to this slot" @@ -81,6 +91,8 @@ class BalStorageSlot(CamelModel, RLPSerializable): class BalAccountChange(CamelModel, RLPSerializable): """Represents all changes to a specific account in a block.""" + model_config = CamelModel.model_config | {"extra": "forbid"} + address: Address = Field(..., description="Account address") nonce_changes: List[BalNonceChange] = Field( default_factory=list, description="List of nonce changes" diff --git a/src/ethereum_test_types/block_access_list/expectations.py b/src/ethereum_test_types/block_access_list/expectations.py index f60b3c2ea76..84c006ca5ff 100644 --- a/src/ethereum_test_types/block_access_list/expectations.py +++ b/src/ethereum_test_types/block_access_list/expectations.py @@ -32,6 +32,8 @@ class BalAccountExpectation(CamelModel): used for expectations. """ + model_config = CamelModel.model_config | {"extra": "forbid"} + nonce_changes: List[BalNonceChange] = Field( default_factory=list, description="List of expected nonce changes" ) From b6075f2486747d01d8e61c8fc812cd154f6a82b3 Mon Sep 17 00:00:00 2001 From: fselmo Date: Sun, 28 Sep 2025 17:52:48 -0600 Subject: [PATCH 19/25] point to latest commit in specs --- .github/configs/eels_resolutions.json | 2 +- src/pytest_plugins/eels_resolutions.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/configs/eels_resolutions.json b/.github/configs/eels_resolutions.json index 9177db60bd6..9fe14efc946 100644 --- a/.github/configs/eels_resolutions.json +++ b/.github/configs/eels_resolutions.json @@ -52,6 +52,6 @@ "Amsterdam": { "git_url": "https://github.com/fselmo/execution-specs.git", "branch": "feat/amsterdam-fork-and-block-access-lists", - "commit": "08b46ef83300da7a47f9ee18884620e1700259e8" + "commit": "6abe0ecb792265211d93bc3fea2f932e5d2cfa90" } } diff --git a/src/pytest_plugins/eels_resolutions.json b/src/pytest_plugins/eels_resolutions.json index 2f783714338..f7b1dd99081 100644 --- a/src/pytest_plugins/eels_resolutions.json +++ b/src/pytest_plugins/eels_resolutions.json @@ -55,6 +55,6 @@ "Amsterdam": { "git_url": "https://github.com/fselmo/execution-specs.git", "branch": "feat/amsterdam-fork-and-block-access-lists", - "commit": "08b46ef83300da7a47f9ee18884620e1700259e8" + "commit": "6abe0ecb792265211d93bc3fea2f932e5d2cfa90" } } From 097e36c47e553b3515e3c38d55632a2cd5101635 Mon Sep 17 00:00:00 2001 From: fselmo Date: Thu, 25 Sep 2025 13:22:44 -0600 Subject: [PATCH 20/25] feat(tests): add 7028 test for pre-funded selfdestruct account - Add a parametrized case for pre-funded selfdestruct account. In this case, the account was funded before the self-destruct, so we do record the balance post-state as being `0` since there is an actual balance change between pre-state and post-state. - The case where a same-transaction selfdestruct account was not pre-funded, there is no net balance change between pre-state and post-state so we don't record any balance change in the BAL. --- .../test_block_access_lists.py | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) 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 d6cbc627cdf..51561be51d6 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 @@ -289,14 +289,19 @@ def test_bal_code_changes( @pytest.mark.valid_from("Amsterdam") @pytest.mark.parametrize( - "self_destruct_in_same_tx", - [True, False], - ids=["self_destruct_in_same_tx", "self_destruct_in_a_new_tx"], + "self_destruct_in_same_tx,pre_funded", + [[True, True], [True, False], [False, False]], + ids=[ + "self_destruct_in_same_tx_pre_funded", + "self_destruct_in_same_tx_not_pre_funded", + "self_destruct_in_a_new_tx", + ], ) def test_bal_self_destruct( pre: Alloc, blockchain_test: BlockchainTestFiller, self_destruct_in_same_tx: bool, + pre_funded: bool, ): """Ensure BAL captures balance changes caused by `SELFDESTRUCT`.""" alice = pre.fund_eoa() @@ -310,18 +315,24 @@ def test_bal_self_destruct( self_destruct_init_code = Initcode(deploy_code=selfdestruct_code) template = pre.deploy_contract(code=self_destruct_init_code) + funds_to_expend = 100 + if self_destruct_in_same_tx: # The goal is to create a self-destructing contract in the same # transaction to trigger deletion of code as per EIP-6780. # The factory contract below creates a new self-destructing # contract and calls it in this transaction. + if pre_funded: + pre_fund_amount = 1 + funds_to_expend -= pre_fund_amount + bytecode_size = len(self_destruct_init_code) factory_bytecode = ( # Clone template memory Op.EXTCODECOPY(template, 0, 0, bytecode_size) # Fund 100 wei and deploy the clone - + Op.CREATE(100, 0, bytecode_size) + + Op.CREATE(funds_to_expend, 0, bytecode_size) # Call the clone, which self-destructs + Op.CALL(50_000, Op.DUP6, 0, 0, 0, 0, 0) + Op.STOP @@ -330,10 +341,14 @@ def test_bal_self_destruct( factory = pre.deploy_contract(code=factory_bytecode) kaboom_same_tx = compute_create_address(address=factory, nonce=1) + if pre_funded: + # pre-fund `1` + pre.fund_address(address=kaboom_same_tx, amount=pre_fund_amount) + tx = Transaction( sender=alice, to=factory if self_destruct_in_same_tx else kaboom, - value=100, + value=funds_to_expend, gas_limit=1_000_000, ) @@ -351,16 +366,11 @@ def test_bal_self_destruct( balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)] ), self_destructed_account: BalAccountExpectation( - # When an account is deleted its post nonce is set to 0 - # as per EIP-7928. - nonce_changes=[BalNonceChange(tx_index=1, post_nonce=0)] - if self_destruct_in_same_tx - else [], - balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)], - # Expect code to be cleared if self-destructed in same transaction. - code_changes=[BalCodeChange(tx_index=1, new_code="")] - if self_destruct_in_same_tx + balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)] + if pre_funded else [], + code_changes=[], # should not be present + nonce_changes=[], # should not be present ), } ), From 366e454b3c9eebc877d061f75647cdfdabfd8632 Mon Sep 17 00:00:00 2001 From: fselmo Date: Sun, 28 Sep 2025 18:06:53 -0600 Subject: [PATCH 21/25] fix: rebase with upstream and fix lint --- .../block_access_list/expectations.py | 3 ++- src/ethereum_test_types/tests/test_block_access_lists.py | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ethereum_test_types/block_access_list/expectations.py b/src/ethereum_test_types/block_access_list/expectations.py index 84c006ca5ff..5a69d45e7dc 100644 --- a/src/ethereum_test_types/block_access_list/expectations.py +++ b/src/ethereum_test_types/block_access_list/expectations.py @@ -238,7 +238,8 @@ def _validate_bal_ordering(bal: "BlockAccessList") -> None: f"{account.storage_changes[i].slot}" ) - # Check transaction index ordering and uniqueness within storage slots + # Check transaction index ordering and uniqueness within storage + # slots for storage_slot in account.storage_changes: if not storage_slot.slot_changes: continue diff --git a/src/ethereum_test_types/tests/test_block_access_lists.py b/src/ethereum_test_types/tests/test_block_access_lists.py index c4d9940833b..20ad08b497e 100644 --- a/src/ethereum_test_types/tests/test_block_access_lists.py +++ b/src/ethereum_test_types/tests/test_block_access_lists.py @@ -340,7 +340,9 @@ def test_actual_bal_tx_indices_ordering(field_name): ["nonce_changes", "balance_changes", "code_changes"], ) def test_actual_bal_duplicate_tx_indices(field_name): - """Test that actual BAL must not have duplicate tx indices in change lists.""" + """ + Test that actual BAL must not have duplicate tx indices in change lists. + """ addr = Address(0xA) # Duplicate tx_index=1 @@ -376,7 +378,10 @@ def test_actual_bal_duplicate_tx_indices(field_name): def test_actual_bal_storage_duplicate_tx_indices(): - """Test that storage changes must not have duplicate tx indices within same slot.""" + """ + Test that storage changes must not have duplicate tx indices within same + slot. + """ addr = Address(0xA) # Create storage changes with duplicate tx_index within the same slot From f39106f7cacca41ae415f708349418faff82175a Mon Sep 17 00:00:00 2001 From: raxhvl Date: Mon, 29 Sep 2025 09:44:47 +0000 Subject: [PATCH 22/25] =?UTF-8?q?=E2=9C=A8=20feat(tests):=20test=5Fbal=5Fs?= =?UTF-8?q?elf=5Fdestruct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_block_access_lists.py | 61 +++++++++++-------- .../test_cases.md | 2 +- 2 files changed, 37 insertions(+), 26 deletions(-) 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 51561be51d6..5d0b2314071 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 @@ -288,15 +288,8 @@ def test_bal_code_changes( @pytest.mark.valid_from("Amsterdam") -@pytest.mark.parametrize( - "self_destruct_in_same_tx,pre_funded", - [[True, True], [True, False], [False, False]], - ids=[ - "self_destruct_in_same_tx_pre_funded", - "self_destruct_in_same_tx_not_pre_funded", - "self_destruct_in_a_new_tx", - ], -) +@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( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -307,15 +300,20 @@ def test_bal_self_destruct( alice = pre.fund_eoa() bob = pre.fund_eoa(amount=0) - selfdestruct_code = Op.SELFDESTRUCT(bob) - # A pre existing self-destruct contract - kaboom = pre.deploy_contract(code=selfdestruct_code) + selfdestruct_code = ( + Op.SLOAD(0x01) # Read from storage slot 0x01 + + Op.SSTORE(0x02, 0x42) # Write to storage slot 0x02 + + Op.SELFDESTRUCT(bob) + ) + # A pre existing self-destruct contract with initial storage + kaboom = pre.deploy_contract(code=selfdestruct_code, storage={0x01: 0x123}) # A template for self-destruct contract self_destruct_init_code = Initcode(deploy_code=selfdestruct_code) template = pre.deploy_contract(code=self_destruct_init_code) - funds_to_expend = 100 + transfer_amount = expected_recipient_balance = 100 + pre_fund_amount = 10 if self_destruct_in_same_tx: # The goal is to create a self-destructing contract in the same @@ -323,33 +321,34 @@ def test_bal_self_destruct( # The factory contract below creates a new self-destructing # contract and calls it in this transaction. - if pre_funded: - pre_fund_amount = 1 - funds_to_expend -= pre_fund_amount - bytecode_size = len(self_destruct_init_code) factory_bytecode = ( # Clone template memory Op.EXTCODECOPY(template, 0, 0, bytecode_size) # Fund 100 wei and deploy the clone - + Op.CREATE(funds_to_expend, 0, bytecode_size) + + Op.CREATE(transfer_amount, 0, bytecode_size) # Call the clone, which self-destructs - + Op.CALL(50_000, Op.DUP6, 0, 0, 0, 0, 0) + + Op.CALL(100_000, Op.DUP6, 0, 0, 0, 0, 0) + Op.STOP ) factory = pre.deploy_contract(code=factory_bytecode) kaboom_same_tx = compute_create_address(address=factory, nonce=1) - if pre_funded: - # pre-fund `1` + # Pre fund the accounts in pre state + if pre_funded: + expected_recipient_balance += pre_fund_amount + if self_destruct_in_same_tx: pre.fund_address(address=kaboom_same_tx, amount=pre_fund_amount) + else: + pre.fund_address(address=kaboom, amount=pre_fund_amount) tx = Transaction( sender=alice, to=factory if self_destruct_in_same_tx else kaboom, - value=funds_to_expend, + value=transfer_amount, gas_limit=1_000_000, + gas_price=0xA, ) # Determine which account was destructed @@ -363,12 +362,16 @@ def test_bal_self_destruct( nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)], ), bob: BalAccountExpectation( - balance_changes=[BalBalanceChange(tx_index=1, post_balance=100)] + balance_changes=[ + BalBalanceChange(tx_index=1, post_balance=expected_recipient_balance) + ] ), self_destructed_account: BalAccountExpectation( balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)] if pre_funded else [], + storage_reads=[0x01], # Read from storage slot 0x01 + storage_changes=[], code_changes=[], # should not be present nonce_changes=[], # should not be present ), @@ -378,8 +381,7 @@ def test_bal_self_destruct( post = { alice: Account(nonce=1), - bob: Account(balance=100), - kaboom: Account(balance=0, code=selfdestruct_code), + bob: Account(balance=expected_recipient_balance), } # If the account was self-destructed in the same transaction, @@ -393,6 +395,15 @@ def test_bal_self_destruct( code=factory_bytecode, ), kaboom_same_tx: Account.NONEXISTENT, # type: ignore + # The pre-existing contract remains unaffected + kaboom: Account(balance=0, code=selfdestruct_code, storage={0x01: 0x123}), + } + ) + else: + post.update( + { + # This contract was self-destructed, we want its storage to be cleared + kaboom: Account(balance=0, code=selfdestruct_code, storage={0x01: 0x0, 0x2: 0x0}), } ) 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 7470bd1731b..d8847bdfbd4 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -7,7 +7,7 @@ | `test_bal_storage_writes` | Ensure BAL captures storage writes | Alice calls contract that writes to storage slot `0x01` | BAL MUST include storage changes with correct slot and value | ✅ Completed | | `test_bal_storage_reads` | Ensure BAL captures storage reads | Alice calls contract that reads from storage slot `0x01` | BAL MUST include storage access for the read operation | ✅ Completed | | `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 balance changes caused by `SELFDESTRUCT` | Alice calls a contract (funded with 100 wei) that executes `SELFDESTRUCT` with Bob as its recipient | BAL MUST include Alice's nonce change (increment), the self-destructing contract's balance change (100 → 0), and Bob's balance change (0 → 100). The self-destructing contract MUST also appear with `post_nonce=0`, `post_balance=0`, and `new_code=0x`, indicating an account deletion | ✅ 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_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 | From 31cdd9b0ca943c90392c54421d668cab1cc0a390 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Mon, 29 Sep 2025 10:49:40 +0000 Subject: [PATCH 23/25] =?UTF-8?q?=E2=9C=A8=20feat(tests):=20test=5Fbal=5Fs?= =?UTF-8?q?elf=5Fdestruct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_block_access_lists.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 5d0b2314071..ac687bb97a9 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 @@ -370,7 +370,7 @@ def test_bal_self_destruct( balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)] if pre_funded else [], - storage_reads=[0x01], # Read from storage slot 0x01 + storage_reads=[0x01, 0x42], # Accessed slots to be recorded as reads storage_changes=[], code_changes=[], # should not be present nonce_changes=[], # should not be present @@ -402,8 +402,12 @@ def test_bal_self_destruct( else: post.update( { - # This contract was self-destructed, we want its storage to be cleared - kaboom: Account(balance=0, code=selfdestruct_code, storage={0x01: 0x0, 0x2: 0x0}), + # This contract was self-destructed in a separate tx. + # From EIP 6780: `SELFDESTRUCT` does not delete any data + # (including storage keys, code, or the account itself). + kaboom: Account( + balance=0, code=selfdestruct_code, storage={0x01: 0x123, 0x2: 0x42} + ), } ) From b290901920548b7950bb002ace2ce4f00e547e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= <51536394+nerolation@users.noreply.github.com> Date: Mon, 29 Sep 2025 13:08:32 +0200 Subject: [PATCH 24/25] Add more complex selfdestruct tests (#2207) * Add more complex selfdestruct tests * Add tests for EIP-7928 around precompiles --- .../amsterdam/eip7928_block_level_access_lists/test_cases.md | 5 +++++ 1 file changed, 5 insertions(+) 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 d8847bdfbd4..d5b3d2c11d9 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_cases.md @@ -15,5 +15,10 @@ | `test_bal_system_contracts_2935_4788` | BAL includes pre-exec system writes for parent hash & beacon root | Build a block with `N` normal txs; 2935 & 4788 active | BAL MUST include `HISTORY_STORAGE_ADDRESS` (EIP-2935) and `BEACON_ROOTS_ADDRESS` (EIP-4788) with `storage_changes` to ring-buffer slots; each write uses `tx_index = N` (system op). | 🟡 Planned | | `test_bal_system_dequeue_withdrawals_eip7002` | BAL tracks post-exec system dequeues for withdrawals | Pre-populate EIP-7002 withdrawal requests; produce a block where dequeues occur | BAL MUST include the 7002 system contract with `storage_changes` (queue head/tail slots 0–3) using `tx_index = len(txs)` and balance changes for withdrawal recipients. | 🟡 Planned | | `test_bal_system_dequeue_consolidations_eip7251` | BAL tracks post-exec system dequeues for consolidations | Pre-populate EIP-7251 consolidation requests; produce a block where dequeues occur | BAL MUST include the 7251 system contract with `storage_changes` (queue slots 0–3) using `tx_index = len(txs)`. | 🟡 Planned | +| `test_bal_create2_to_A_read_then_selfdestruct` | BAL records balance change for A and storage access (no persistent change) | Tx0: Alice sends ETH to address **A**. Tx1: Deployer `CREATE2` a contract **at A**; contract does `SLOAD(B)` and immediately `SELFDESTRUCT(beneficiary=X)` in the same tx. | BAL **MUST** include **A** with `balance_changes` (funding in Tx0 and transfer on selfdestruct in Tx1). BAL **MUST** include storage key **B** as an accessed `StorageKey`, and **MUST NOT** include **B** under `storage_changes` (no persistence due to same-tx create+destruct). | 🟡 Planned | +| `test_bal_create2_to_A_write_then_selfdestruct` | BAL records balance change for A and storage access even if a write occurred (no persistent change) | Tx0: Alice sends ETH to **A**. Tx1: Deployer `CREATE2` contract **at A**; contract does `SSTORE(B, v)` (optionally `SLOAD(B)`), then `SELFDESTRUCT(beneficiary=Y)` in the same tx. | BAL **MUST** include **A** with `balance_changes` (Tx0 fund; Tx1 outflow to `Y`). BAL **MUST** include **B** as `StorageKey` accessed, and **MUST NOT** include **B** under `storage_changes` (ephemeral write discarded because the contract was created and destroyed in the same tx). | 🟡 Planned | +| `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 | + > ℹ️ Scope describes whether a test spans a single transaction (`tx`) or entire block (`blk`). From ac1e471007e68be0397ad68e55847311ae01216b Mon Sep 17 00:00:00 2001 From: fselmo Date: Mon, 29 Sep 2025 11:21:16 -0600 Subject: [PATCH 25/25] fix(tests): Fix expectations for self-destruct tests --- .../test_block_access_lists.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) 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 ac687bb97a9..1f53ca1b211 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 @@ -1,7 +1,10 @@ """Tests for EIP-7928 using the consistent data class pattern.""" +from typing import Dict + import pytest +from ethereum_test_base_types import Address from ethereum_test_tools import ( Account, Alloc, @@ -335,13 +338,12 @@ def test_bal_self_destruct( factory = pre.deploy_contract(code=factory_bytecode) kaboom_same_tx = compute_create_address(address=factory, nonce=1) - # Pre fund the accounts in pre state + # Determine which account will be self-destructed + self_destructed_account = kaboom_same_tx if self_destruct_in_same_tx else kaboom + if pre_funded: expected_recipient_balance += pre_fund_amount - if self_destruct_in_same_tx: - pre.fund_address(address=kaboom_same_tx, amount=pre_fund_amount) - else: - pre.fund_address(address=kaboom, amount=pre_fund_amount) + pre.fund_address(address=self_destructed_account, amount=pre_fund_amount) tx = Transaction( sender=alice, @@ -351,9 +353,6 @@ def test_bal_self_destruct( gas_price=0xA, ) - # Determine which account was destructed - self_destructed_account = kaboom_same_tx if self_destruct_in_same_tx else kaboom - block = Block( txs=[tx], expected_block_access_list=BlockAccessListExpectation( @@ -370,8 +369,17 @@ def test_bal_self_destruct( balance_changes=[BalBalanceChange(tx_index=1, post_balance=0)] if pre_funded else [], - storage_reads=[0x01, 0x42], # Accessed slots to be recorded as reads - storage_changes=[], + # Accessed slots for same-tx are recorded as reads (0x02) + storage_reads=[0x01, 0x02] if self_destruct_in_same_tx else [0x01], + # Storage changes are recorded for non-same-tx + # self-destructs + storage_changes=[ + BalStorageSlot( + slot=0x02, slot_changes=[BalStorageChange(tx_index=1, post_value=0x42)] + ) + ] + if not self_destruct_in_same_tx + else [], code_changes=[], # should not be present nonce_changes=[], # should not be present ), @@ -379,7 +387,7 @@ def test_bal_self_destruct( ), ) - post = { + post: Dict[Address, Account] = { alice: Account(nonce=1), bob: Account(balance=expected_recipient_balance), }