Skip to content

Commit 5224504

Browse files
committed
fix(spec-specs): Track code per auth; filter pre at tx frame
1 parent c17d91b commit 5224504

File tree

6 files changed

+238
-8
lines changed

6 files changed

+238
-8
lines changed

src/ethereum/forks/amsterdam/block_access_lists/builder.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -515,8 +515,7 @@ def build_block_access_list(
515515
add_nonce_change(builder, address, block_access_index, new_nonce)
516516

517517
# Add all code changes
518-
# Net-zero filtering for code changes should happen at the
519-
# transaction level (in merge_on_success), not at the block level.
518+
# Filtering happens at transaction level in eoa_delegation.py
520519
for (
521520
address,
522521
block_access_index,

src/ethereum/forks/amsterdam/state.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,53 @@ def write_code(sender: Account) -> None:
655655
track_code_change(state_changes, address, code)
656656

657657

658+
def set_authority_code(
659+
state: State,
660+
address: Address,
661+
code: Bytes,
662+
state_changes: StateChanges,
663+
current_code: Bytes,
664+
) -> None:
665+
"""
666+
Sets authority account code for EIP-7702 delegation.
667+
668+
This function is used specifically for setting authority code within
669+
EIP-7702 Set Code Transactions. Unlike set_code(), it tracks changes based
670+
on the current code rather than pre_code to handle multiple authorizations
671+
to the same address within a single transaction correctly.
672+
673+
Parameters
674+
----------
675+
state:
676+
The current state.
677+
678+
address:
679+
Address of the authority account whose code needs to be set.
680+
681+
code:
682+
The delegation designation bytecode to set.
683+
684+
state_changes:
685+
State changes frame for tracking (EIP-7928).
686+
687+
current_code:
688+
The current code before this change. Used to determine if tracking
689+
is needed (only track if code actually changes from current value).
690+
691+
"""
692+
693+
def write_code(sender: Account) -> None:
694+
sender.code = code
695+
696+
modify_state(state, address, write_code)
697+
698+
# Only track if code is actually changing from current value
699+
# This allows multiple auths to same address to be tracked individually
700+
# Net-zero filtering happens in commit_transaction_frame
701+
if current_code != code:
702+
track_code_change(state_changes, address, code)
703+
704+
658705
def get_storage_original(state: State, address: Address, key: Bytes32) -> U256:
659706
"""
660707
Get the original value in a storage slot i.e. the value before the current

src/ethereum/forks/amsterdam/state_tracker.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ def merge_on_success(child_frame: StateChanges) -> None:
372372
parent_frame.pre_storage[slot] = value
373373
for addr, code in child_frame.pre_code.items():
374374
if addr not in parent_frame.pre_code:
375-
parent_frame.pre_code[addr] = code
375+
capture_pre_code(parent_frame, addr, code)
376376

377377
# Merge storage operations, filtering noop writes
378378
parent_frame.storage_reads.update(child_frame.storage_reads)
@@ -458,9 +458,13 @@ def commit_transaction_frame(tx_frame: StateChanges) -> None:
458458
for addr, idx, nonce in tx_frame.nonce_changes:
459459
block_frame.nonce_changes.add((addr, idx, nonce))
460460

461-
# Merge code changes
461+
# Merge code changes - filter net-zero changes within the transaction
462+
# Compare final code against transaction's pre-code
462463
for (addr, idx), final_code in tx_frame.code_changes.items():
463-
block_frame.code_changes[(addr, idx)] = final_code
464+
pre_code = tx_frame.pre_code.get(addr, b"")
465+
if pre_code != final_code:
466+
block_frame.code_changes[(addr, idx)] = final_code
467+
# else: Net-zero change within this transaction - skip
464468

465469

466470
def merge_on_failure(child_frame: StateChanges) -> None:

src/ethereum/forks/amsterdam/vm/eoa_delegation.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414

1515
# track_address_access removed - now using state_changes.track_address()
1616
from ..fork_types import Address, Authorization
17-
from ..state import account_exists, get_account, increment_nonce, set_code
17+
from ..state import (
18+
account_exists,
19+
get_account,
20+
increment_nonce,
21+
set_authority_code,
22+
)
1823
from ..state_tracker import capture_pre_code, track_address
1924
from ..utils.hexadecimal import hex_to_address
2025
from ..vm.gas import GAS_COLD_ACCOUNT_ACCESS, GAS_WARM_ACCESS
@@ -253,8 +258,16 @@ def set_delegation(message: Message) -> U256:
253258
or message.block_env.block_state_changes
254259
)
255260

261+
# Capture pre-code before any changes (first-write-wins)
256262
capture_pre_code(state_changes, authority, authority_code)
257-
set_code(state, authority, code_to_set, state_changes)
263+
264+
# Set delegation code
265+
# Uses authority_code (current) for tracking to handle multiple auths
266+
# Net-zero filtering happens in commit_transaction_frame
267+
set_authority_code(
268+
state, authority, code_to_set, state_changes, authority_code
269+
)
270+
258271
increment_nonce(state, authority, state_changes)
259272

260273
if message.code_address is None:

tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,3 +663,168 @@ def test_bal_7702_null_address_delegation_no_code_change(
663663
bob: Account(balance=10),
664664
},
665665
)
666+
667+
668+
def test_bal_7702_double_auth_reset(
669+
pre: Alloc,
670+
blockchain_test: BlockchainTestFiller,
671+
) -> None:
672+
"""
673+
Ensure BAL captures the net code change when multiple authorizations
674+
occur in the same transaction (double auth).
675+
676+
This test verifies that when:
677+
1. First auth sets delegation to CONTRACT_A
678+
2. Second auth resets delegation to empty (address 0)
679+
680+
The BAL should show the NET change (empty -> empty), not intermediate
681+
states. This is a regression test for the bug where the BAL showed
682+
the first auth's code but the final state was empty.
683+
"""
684+
alice = pre.fund_eoa()
685+
bob = pre.fund_eoa(amount=0)
686+
relayer = pre.fund_eoa()
687+
688+
contract_a = pre.deploy_contract(code=Op.STOP)
689+
690+
# Transaction with double auth:
691+
# 1. First sets delegation to contract_a
692+
# 2. Second resets to empty
693+
tx = Transaction(
694+
sender=relayer,
695+
to=bob,
696+
value=10,
697+
gas_limit=1_000_000,
698+
gas_price=0xA,
699+
authorization_list=[
700+
AuthorizationTuple(
701+
address=contract_a,
702+
nonce=0,
703+
signer=alice,
704+
),
705+
AuthorizationTuple(
706+
address=0, # Reset to empty
707+
nonce=1,
708+
signer=alice,
709+
),
710+
],
711+
)
712+
713+
blockchain_test(
714+
pre=pre,
715+
blocks=[
716+
Block(
717+
txs=[tx],
718+
expected_block_access_list=BlockAccessListExpectation(
719+
account_expectations={
720+
alice: BalAccountExpectation(
721+
nonce_changes=[
722+
BalNonceChange(tx_index=1, post_nonce=2)
723+
],
724+
code_changes=[],
725+
),
726+
bob: BalAccountExpectation(
727+
balance_changes=[
728+
BalBalanceChange(tx_index=1, post_balance=10)
729+
]
730+
),
731+
relayer: BalAccountExpectation(
732+
nonce_changes=[
733+
BalNonceChange(tx_index=1, post_nonce=1)
734+
],
735+
),
736+
contract_a: None,
737+
}
738+
),
739+
)
740+
],
741+
post={
742+
alice: Account(nonce=2, code=b""), # Final code is empty
743+
bob: Account(balance=10),
744+
relayer: Account(nonce=1),
745+
},
746+
)
747+
748+
749+
def test_bal_7702_double_auth_swap(
750+
pre: Alloc,
751+
blockchain_test: BlockchainTestFiller,
752+
) -> None:
753+
"""
754+
Ensure BAL captures the net code change when double auth swaps
755+
delegation targets.
756+
757+
This test verifies that when:
758+
1. First auth sets delegation to CONTRACT_A
759+
2. Second auth changes delegation to CONTRACT_B
760+
761+
The BAL should show the final code change (empty -> CONTRACT_B),
762+
not the intermediate CONTRACT_A.
763+
"""
764+
alice = pre.fund_eoa()
765+
bob = pre.fund_eoa(amount=0)
766+
relayer = pre.fund_eoa()
767+
768+
contract_a = pre.deploy_contract(code=Op.STOP)
769+
contract_b = pre.deploy_contract(code=Op.STOP)
770+
771+
tx = Transaction(
772+
sender=relayer,
773+
to=bob,
774+
value=10,
775+
gas_limit=1_000_000,
776+
gas_price=0xA,
777+
authorization_list=[
778+
AuthorizationTuple(
779+
address=contract_a,
780+
nonce=0,
781+
signer=alice,
782+
),
783+
AuthorizationTuple(
784+
address=contract_b, # Override to contract_b
785+
nonce=1,
786+
signer=alice,
787+
),
788+
],
789+
)
790+
791+
account_expectations = {
792+
alice: BalAccountExpectation(
793+
nonce_changes=[BalNonceChange(tx_index=1, post_nonce=2)],
794+
code_changes=[
795+
# Should show final code (CONTRACT_B), not CONTRACT_A
796+
BalCodeChange(
797+
tx_index=1,
798+
new_code=Spec7702.delegation_designation(contract_b),
799+
)
800+
],
801+
),
802+
bob: BalAccountExpectation(
803+
balance_changes=[BalBalanceChange(tx_index=1, post_balance=10)]
804+
),
805+
relayer: BalAccountExpectation(
806+
nonce_changes=[BalNonceChange(tx_index=1, post_nonce=1)],
807+
),
808+
# Neither contract appears in BAL during delegation setup
809+
contract_a: None,
810+
contract_b: None,
811+
}
812+
813+
blockchain_test(
814+
pre=pre,
815+
blocks=[
816+
Block(
817+
txs=[tx],
818+
expected_block_access_list=BlockAccessListExpectation(
819+
account_expectations=account_expectations
820+
),
821+
)
822+
],
823+
post={
824+
alice: Account(
825+
nonce=2, code=Spec7702.delegation_designation(contract_b)
826+
),
827+
bob: Account(balance=10),
828+
relayer: Account(nonce=1),
829+
},
830+
)

tests/amsterdam/eip7928_block_level_access_lists/test_cases.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
| `test_bal_7702_invalid_nonce_authorization` | Ensure BAL handles failed authorization due to wrong nonce | `Relayer` sends sponsored transaction to Bob (10 wei transfer succeeds) but Alice's authorization to delegate to `Oracle` uses incorrect nonce, causing silent authorization failure | BAL **MUST** include Alice with empty changes (account access), Bob with `balance_changes` (receives 10 wei), Relayer with `nonce_changes`. **MUST NOT** include `Oracle` (authorization failed, no delegation) | ✅ Completed |
4040
| `test_bal_7702_invalid_chain_id_authorization` | Ensure BAL handles failed authorization due to wrong chain id | `Relayer` sends sponsored transaction to Bob (10 wei transfer succeeds) but Alice's authorization to delegate to `Oracle` uses incorrect chain id, causing authorization failure before account access | BAL **MUST** include Bob with `balance_changes` (receives 10 wei), Relayer with `nonce_changes`. **MUST NOT** include Alice (authorization fails before loading account) or `Oracle` (authorization failed, no delegation) | ✅ Completed |
4141
| `test_bal_7702_delegated_via_call_opcode` | Ensure BAL captures delegation target when a contract uses *CALL opcodes to call a delegated account | Pre-deployed contract `Alice` delegated to `Oracle`. `Caller` contract uses CALL/CALLCODE/DELEGATECALL/STATICCALL to call `Alice`. Bob sends transaction to `Caller`. | BAL **MUST** include Bob: `nonce_changes`. `Caller`: empty changes (account access). `Alice`: empty changes (account access - delegated account being called). `Oracle`: empty changes (delegation target access). | ✅ Completed |
42-
| `test_bal_7702_null_address_delegation` | Ensure BAL does not record spurious code changes for net-zero code operations | Alice sends transaction with authorization delegating to NULL_ADDRESS (0x0), which sets code to `b""` on an account that already has `b""` code. Transaction sends 10 wei to Bob. | BAL **MUST** include Alice with `nonce_changes` (tx nonce + auth nonce increment) but **MUST NOT** include `code_changes` (setting `b"" -> b""` is net-zero and filtered out). Bob: `balance_changes` (receives 10 wei). This ensures net-zero code change is not recorded.
42+
| `test_bal_7702_null_address_delegation` | Ensure BAL does not record spurious code changes for net-zero code operations | Alice sends transaction with authorization delegating to NULL_ADDRESS (0x0), which sets code to `b""` on an account that already has `b""` code. Transaction sends 10 wei to Bob. | BAL **MUST** include Alice with `nonce_changes` (tx nonce + auth nonce increment) but **MUST NOT** include `code_changes` (setting `b"" -> b""` is net-zero and filtered out). Bob: `balance_changes` (receives 10 wei). This ensures net-zero code change is not recorded. | ✅ Completed |
43+
| `test_bal_7702_double_auth_reset` | Ensure BAL captures net code change when double auth resets delegation | `Relayer` sends transaction with two authorizations for Alice: (1) First auth sets delegation to `CONTRACT_A` at nonce=0, (2) Second auth resets delegation to empty (address 0) at nonce=1. Transaction sends 10 wei to Bob. Per EIP-7702, only the last authorization takes effect. | BAL **MUST** include Alice with `nonce_changes` (both auths increment nonce to 2) but **MUST NOT** include `code_changes` (net change is empty → empty). Bob: `balance_changes` (receives 10 wei). Relayer: `nonce_changes`. `CONTRACT_A` **MUST NOT** be in BAL (never accessed). This is a regression test for the bug where BAL showed first auth's code despite final state being empty. | ✅ Completed |
44+
| `test_bal_7702_double_auth_swap` | Ensure BAL captures final code when double auth swaps delegation targets | `Relayer` sends transaction with two authorizations for Alice: (1) First auth sets delegation to `CONTRACT_A` at nonce=0, (2) Second auth changes delegation to `CONTRACT_B` at nonce=1. Transaction sends 10 wei to Bob. Per EIP-7702, only the last authorization takes effect. | BAL **MUST** include Alice with `nonce_changes` (both auths increment nonce to 2) and `code_changes` (final code is delegation designation for `CONTRACT_B`, not `CONTRACT_A`). Bob: `balance_changes` (receives 10 wei). Relayer: `nonce_changes`. Neither `CONTRACT_A` nor `CONTRACT_B` appear in BAL during delegation setup (never accessed). This ensures BAL shows final state, not intermediate changes. | ✅ Completed |
4345
| `test_bal_sstore_and_oog` | Ensure BAL handles OOG during SSTORE execution at various gas boundaries (EIP-2200 stipend and implicit SLOAD) | Alice calls contract that attempts `SSTORE` to cold slot `0x01`. Parameterized: (1) OOG at EIP-2200 stipend check (2300 gas after PUSH opcodes) - fails before implicit SLOAD, (2) OOG at stipend + 1 (2301 gas) - passes stipend check but fails after implicit SLOAD, (3) OOG at exact gas - 1, (4) Successful SSTORE with exact gas. | For case (1): BAL **MUST NOT** include slot `0x01` in `storage_reads` or `storage_changes` (fails before implicit SLOAD). For cases (2) and (3): BAL **MUST** include slot `0x01` in `storage_reads` (implicit SLOAD occurred) but **MUST NOT** include in `storage_changes` (write didn't complete). For case (4): BAL **MUST** include slot `0x01` in `storage_changes` only (successful write; read is filtered by builder). | ✅ Completed |
4446
| `test_bal_sstore_static_context` | Ensure BAL does not capture spurious storage access when SSTORE fails in static context | Alice calls contract with `STATICCALL` which attempts `SSTORE` to slot `0x01`. SSTORE must fail before any storage access occurs. | BAL **MUST NOT** include slot `0x01` in `storage_reads` or `storage_changes`. Static context check happens before storage access, preventing spurious reads. Alice has `nonce_changes` and `balance_changes` (gas cost). Target contract included with empty changes. | ✅ Completed |
4547
| `test_bal_sload_and_oog` | Ensure BAL handles OOG during SLOAD execution correctly | Alice calls contract that attempts `SLOAD` from cold slot `0x01`. Parameterized: (1) OOG at SLOAD opcode (insufficient gas), (2) Successful SLOAD execution. | For OOG case: BAL **MUST NOT** contain slot `0x01` in `storage_reads` since storage wasn't accessed. For success case: BAL **MUST** contain slot `0x01` in `storage_reads`. | ✅ Completed |

0 commit comments

Comments
 (0)