Skip to content

Commit 55f61ab

Browse files
raxhvlmarioevz
andauthored
✨ feat(test): EIP-7928 Selfdestruct a dirty account (#2967)
* ✨ feat(test): selfdestruct a drity account * nit Co-authored-by: Mario Vega <marioevz@gmail.com> * nit Co-authored-by: Mario Vega <marioevz@gmail.com> * nit Co-authored-by: Mario Vega <marioevz@gmail.com> * nit Co-authored-by: Mario Vega <marioevz@gmail.com> * nit Co-authored-by: Mario Vega <marioevz@gmail.com> * ✨ feat: Parameterise success / revert * 🧹 chore: lint * 🐞 fix: 8037 pricing change; forward all gas --------- Co-authored-by: raxhvl <raxhvl@users.noreply.github.com> Co-authored-by: Mario Vega <marioevz@gmail.com>
1 parent bb030d0 commit 55f61ab

2 files changed

Lines changed: 207 additions & 0 deletions

File tree

tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3744,3 +3744,209 @@ def test_bal_create2_selfdestruct_then_recreate_same_block(
37443744
factory: Account(nonce=3, storage={0: target_a, 1: 1}),
37453745
},
37463746
)
3747+
3748+
3749+
@pytest.mark.parametrize(
3750+
"destruction_successful,oracle_suffix",
3751+
[
3752+
pytest.param(True, Op.STOP, id="destruction_succeeds"),
3753+
pytest.param(False, Op.REVERT(0, 0), id="destruction_reverts"),
3754+
],
3755+
)
3756+
@pytest.mark.with_all_create_opcodes
3757+
def test_bal_dirty_account_selfdestruct(
3758+
pre: Alloc,
3759+
blockchain_test: BlockchainTestFiller,
3760+
create_opcode: Op,
3761+
destruction_successful: bool,
3762+
oracle_suffix: Bytecode,
3763+
) -> None:
3764+
"""
3765+
BAL records dirty state changes on an ephemeral contract only when
3766+
its same-tx SELFDESTRUCT is rolled back by a reverting parent
3767+
frame.
3768+
3769+
The factory deploys the ephemeral with non-zero endowment (balance
3770+
dirty), initcode SSTOREs and SLOADs own slots (storage dirty),
3771+
invokes an empty CREATE so the ephemeral's own nonce bumps 1→2
3772+
(nonce dirty), and returns runtime (code dirty). The factory then
3773+
CALLs an oracle which CALLs the ephemeral's runtime
3774+
(SELFDESTRUCTs), and either STOPs or REVERTs.
3775+
3776+
- destruction_succeeds: oracle STOPs; per EIP-6780 the same-tx
3777+
selfdestruct fully removes the ephemeral; per EIP-7928 its BAL
3778+
entry must contain no balance/nonce/code/storage changes — only
3779+
`storage_reads` for the demoted slots.
3780+
3781+
- destruction_reverts: oracle REVERTs; the SELFDESTRUCT (and the
3782+
balance transfer to the beneficiary) are rolled back. The
3783+
ephemeral persists with all four dirtied fields, which BAL must
3784+
now record.
3785+
"""
3786+
alice = pre.fund_eoa()
3787+
beneficiary = pre.nonexistent_account()
3788+
factory_balance = 1000
3789+
endowment = 100
3790+
slot_write = 0x07
3791+
slot_read = 0x09
3792+
3793+
init_code = Initcode(
3794+
deploy_code=Op.SELFDESTRUCT(beneficiary),
3795+
initcode_prefix=(
3796+
Op.SSTORE(slot_write, 0xCAFE)
3797+
+ Op.POP(Op.SLOAD(slot_read))
3798+
+ Op.POP(create_opcode(value=0, offset=0, size=0))
3799+
),
3800+
)
3801+
3802+
# Oracle CALLs whatever address it receives as calldata, then
3803+
# either STOPs (destruction succeeds) or REVERTs (destruction
3804+
# rolled back). Pre-deployed so its own creation doesn't appear
3805+
# in the block's BAL.
3806+
oracle = pre.deploy_contract(
3807+
code=Op.POP(Op.CALL(Op.GAS, Op.CALLDATALOAD(0), 0, 0, 0, 0, 0))
3808+
+ oracle_suffix,
3809+
)
3810+
3811+
factory_code = (
3812+
Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE)
3813+
+ Op.SSTORE(
3814+
0,
3815+
create_opcode(value=endowment, offset=0, size=Op.CALLDATASIZE),
3816+
)
3817+
+ Op.MSTORE(0, Op.SLOAD(0))
3818+
+ Op.POP(Op.CALL(Op.GAS, oracle, 0, 0, 32, 0, 0))
3819+
+ Op.STOP
3820+
)
3821+
factory = pre.deploy_contract(code=factory_code, balance=factory_balance)
3822+
3823+
ephemeral = compute_create_address(
3824+
address=factory,
3825+
nonce=1,
3826+
initcode=init_code,
3827+
opcode=create_opcode,
3828+
)
3829+
zombie = compute_create_address(
3830+
address=ephemeral,
3831+
nonce=1,
3832+
initcode=b"",
3833+
opcode=create_opcode,
3834+
)
3835+
3836+
expected_ephemeral_post: Account | None
3837+
expected_beneficiary_post: Account | None
3838+
if destruction_successful:
3839+
expected_ephemeral_bal = BalAccountExpectation(
3840+
balance_changes=[],
3841+
nonce_changes=[],
3842+
code_changes=[],
3843+
storage_changes=[],
3844+
storage_reads=[slot_write, slot_read],
3845+
)
3846+
expected_beneficiary_bal = BalAccountExpectation(
3847+
balance_changes=[
3848+
BalBalanceChange(block_access_index=1, post_balance=endowment)
3849+
],
3850+
)
3851+
expected_ephemeral_post = Account.NONEXISTENT
3852+
expected_beneficiary_post = Account(balance=endowment)
3853+
else:
3854+
expected_ephemeral_bal = BalAccountExpectation(
3855+
balance_changes=[
3856+
BalBalanceChange(block_access_index=1, post_balance=endowment)
3857+
],
3858+
nonce_changes=[BalNonceChange(block_access_index=1, post_nonce=2)],
3859+
code_changes=[
3860+
BalCodeChange(
3861+
block_access_index=1, new_code=init_code.deploy_code
3862+
)
3863+
],
3864+
storage_changes=[
3865+
BalStorageSlot(
3866+
slot=slot_write,
3867+
slot_changes=[
3868+
BalStorageChange(
3869+
block_access_index=1, post_value=0xCAFE
3870+
)
3871+
],
3872+
)
3873+
],
3874+
storage_reads=[slot_read],
3875+
)
3876+
expected_beneficiary_bal = BalAccountExpectation.empty()
3877+
expected_ephemeral_post = Account(
3878+
nonce=2,
3879+
balance=endowment,
3880+
code=init_code.deploy_code,
3881+
storage={slot_write: 0xCAFE},
3882+
)
3883+
expected_beneficiary_post = Account.NONEXISTENT
3884+
3885+
tx = Transaction(
3886+
sender=alice,
3887+
to=factory,
3888+
data=init_code,
3889+
gas_limit=1_000_000,
3890+
)
3891+
3892+
block = Block(
3893+
txs=[tx],
3894+
expected_block_access_list=BlockAccessListExpectation(
3895+
account_expectations={
3896+
alice: BalAccountExpectation(
3897+
nonce_changes=[
3898+
BalNonceChange(block_access_index=1, post_nonce=1)
3899+
],
3900+
),
3901+
factory: BalAccountExpectation(
3902+
nonce_changes=[
3903+
BalNonceChange(block_access_index=1, post_nonce=2)
3904+
],
3905+
balance_changes=[
3906+
BalBalanceChange(
3907+
block_access_index=1,
3908+
post_balance=factory_balance - endowment,
3909+
)
3910+
],
3911+
storage_changes=[
3912+
BalStorageSlot(
3913+
slot=0,
3914+
slot_changes=[
3915+
BalStorageChange(
3916+
block_access_index=1,
3917+
post_value=ephemeral,
3918+
)
3919+
],
3920+
)
3921+
],
3922+
),
3923+
ephemeral: expected_ephemeral_bal,
3924+
# The zombie is ALWAYS crated
3925+
# since it was deployed inside the factory's frame,
3926+
# which never reverts.
3927+
zombie: BalAccountExpectation(
3928+
nonce_changes=[
3929+
BalNonceChange(block_access_index=1, post_nonce=1)
3930+
],
3931+
),
3932+
oracle: BalAccountExpectation.empty(),
3933+
beneficiary: expected_beneficiary_bal,
3934+
}
3935+
),
3936+
)
3937+
3938+
blockchain_test(
3939+
pre=pre,
3940+
blocks=[block],
3941+
post={
3942+
alice: Account(nonce=1),
3943+
beneficiary: expected_beneficiary_post,
3944+
factory: Account(
3945+
nonce=2,
3946+
balance=factory_balance - endowment,
3947+
storage={0: ephemeral},
3948+
),
3949+
ephemeral: expected_ephemeral_post,
3950+
zombie: Account(nonce=1),
3951+
},
3952+
)

tests/amsterdam/eip7928_block_level_access_lists/test_cases.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,5 @@
183183
| `test_invalid_pre_fork_block_with_bal_hash_field` | Verify clients reject a pre-Amsterdam block whose header carries `block_access_list_hash`. File: `tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py`. | Single block at `timestamp=14_999` with a regular transfer, mutated via `rlp_modifier=Header(block_access_list_hash=Hash(0))` to inject the field into the pre-fork header schema. | Block **MUST** be rejected with `BlockException.INVALID_BLOCK_HASH`: pre-fork clients compute the block hash without the injected field, mismatching the expected hash. | ✅ Completed |
184184
| `test_invalid_post_fork_block_without_bal_hash_field` | Verify clients reject an Amsterdam activation block whose header is missing `block_access_list_hash`. File: `tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py`. | Single block at `timestamp=15_000` with a regular transfer, mutated via `rlp_modifier=Header(block_access_list_hash=Header.REMOVE_FIELD)` so the field is dropped from the header. | Block **MUST** be rejected with `BlockException.INVALID_BAL_HASH` or `BlockException.INVALID_BLOCK_HASH`: clients re-derive the BAL hash from execution and detect the mismatch either at the BAL hash check or the header hash check. | ✅ Completed |
185185
| `test_fork_transition_bal_size_constraint` | Verify the BAL size constraint (`bal_items <= gas_limit // BLOCK_ACCESS_LIST_ITEM`) applies only on/after Amsterdam. File: `tests/amsterdam/eip7928_block_level_access_lists/test_fork_transition.py`. Parametrized over `exceeds_limit_at_fork`: `at_fork_within_budget` (`gas_limit == empty_block_bal_item_count() * BLOCK_ACCESS_LIST_ITEM`) and `at_fork_over_budget` (`gas_limit` one wei below that). | Two empty blocks: pre-fork (`timestamp=14_999`) and activation block (`timestamp=15_000`). The same low `gas_limit` is used for both via `genesis_environment=Environment(gas_limit=...)`. | Pre-fork block **MUST** be accepted under both budgets (constraint not yet enforced). Activation block **MUST** be accepted at the exact budget and **MUST** be rejected with `BlockException.BLOCK_ACCESS_LIST_GAS_LIMIT_EXCEEDED` one item over the budget. | ✅ Completed |
186+
| `test_bal_dirty_account_selfdestruct` | Ensure BAL does not record dirty state on a same-tx ephemeral contract whose `SELFDESTRUCT` takes effect. | A factory deploys an ephemeral whose initcode dirties balance, nonce, code, and storage; runtime `SELFDESTRUCT` routes through an intermediate oracle. | **success**: ephemeral's BAL entry contains only `storage_reads` for the demoted slots. **revert**: BAL records all four dirty fields (destruction rolled back). | ✅ Completed |
186187

0 commit comments

Comments
 (0)