Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,13 @@ def test_bal_2935_query(
[block_hash_slot] if is_valid else []
)

# Add balance changes if value is transferred and query is valid
if value > 0 and is_valid:
account_expectations[HISTORY_STORAGE_ADDRESS].balance_changes = [
BalBalanceChange(block_access_index=1, post_balance=value)
]
# Balance changes for callee: credited if valid, must be empty if invalid
if value > 0:
account_expectations[HISTORY_STORAGE_ADDRESS].balance_changes = (
[BalBalanceChange(block_access_index=1, post_balance=value)]
if is_valid
else []
)

account_expectations[alice] = BalAccountExpectation(
nonce_changes=[BalNonceChange(block_access_index=1, post_nonce=1)],
Expand Down Expand Up @@ -426,6 +428,8 @@ def test_bal_2935_invalid_calldata_size(
account_expectations = block_hash_system_call_expectations(1)
# History storage contract reverts before any storage access
account_expectations[HISTORY_STORAGE_ADDRESS].storage_reads = []
if value > 0:
account_expectations[HISTORY_STORAGE_ADDRESS].balance_changes = []

account_expectations[alice] = BalAccountExpectation(
nonce_changes=[BalNonceChange(block_access_index=1, post_nonce=1)],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,11 +321,13 @@ def test_bal_4788_query(
else [timestamp_slot]
)

# Add balance changes if value is transferred
if value > 0 and is_valid:
account_expectations[BEACON_ROOTS_ADDRESS].balance_changes = [
BalBalanceChange(block_access_index=1, post_balance=value)
]
# Balance changes for callee: credited if valid, must be empty if invalid
if value > 0:
account_expectations[BEACON_ROOTS_ADDRESS].balance_changes = (
[BalBalanceChange(block_access_index=1, post_balance=value)]
if is_valid
else []
)

# Add transaction-specific expectations
account_expectations[alice] = BalAccountExpectation(
Expand Down Expand Up @@ -386,6 +388,120 @@ def test_bal_4788_query(
)


@pytest.mark.parametrize(
"calldata_size",
[
pytest.param(0, id="empty_calldata"),
pytest.param(31, id="calldata_too_short"),
pytest.param(33, id="calldata_too_long"),
],
)
@pytest.mark.parametrize(
"value",
[
pytest.param(0, id="no_value"),
pytest.param(100, id="with_value"),
],
)
def test_bal_4788_invalid_calldata_size(
pre: Alloc,
blockchain_test: BlockchainTestFiller,
fork: Fork,
calldata_size: int,
value: int,
) -> None:
"""
Ensure BAL correctly handles EIP-4788 queries with invalid calldata size.

EIP-4788 requires exactly 32 bytes of calldata (a timestamp). Any other
size causes immediate revert before any storage access occurs.

Test scenarios with and without value transfer:
1. Empty calldata (0 bytes): Reverts immediately
2. Too short (31 bytes): Reverts before storage access
3. Too long (33 bytes): Reverts before storage access
"""
alice = pre.fund_eoa()

block_timestamp = 12
beacon_root = Hash(0xABCDEF)

# Contract that calls beacon roots contract with variable-size calldata
# and stores returned beacon root in slot 0
query_code = (
Op.CALLDATACOPY(0, 0, calldata_size)
+ Op.CALL(
Spec.BEACON_ROOTS_CALL_GAS,
BEACON_ROOTS_ADDRESS,
Op.CALLVALUE,
0,
calldata_size,
32,
32,
)
+ Op.SSTORE(0, Op.MLOAD(32))
)
query_contract = pre.deploy_contract(query_code)

# Pad calldata to requested size
calldata = b"\x00" * calldata_size

tx = Transaction(
sender=alice,
to=query_contract,
data=calldata,
value=value,
gas_limit=fork.transaction_gas_limit_cap(),
)

account_expectations = beacon_root_system_call_expectations(
block_timestamp, beacon_root
)
# Beacon roots contract reverts before any storage access
account_expectations[BEACON_ROOTS_ADDRESS].storage_reads = []
if value > 0:
account_expectations[BEACON_ROOTS_ADDRESS].balance_changes = []

account_expectations[alice] = BalAccountExpectation(
nonce_changes=[BalNonceChange(block_access_index=1, post_nonce=1)],
)

account_expectations[query_contract] = BalAccountExpectation(
# SSTORE(0, 0) is a no-op write, becomes implicit read
storage_reads=[0],
storage_changes=[],
# Value stays in query contract when call reverts
balance_changes=[
BalBalanceChange(block_access_index=1, post_balance=value)
]
if value > 0
else [],
)

block = Block(
txs=[tx],
parent_beacon_block_root=beacon_root,
timestamp=block_timestamp,
expected_block_access_list=BlockAccessListExpectation(
account_expectations=account_expectations
),
)

post_state: dict[Address, Account] = {
alice: Account(nonce=1),
query_contract: Account(storage={0: 0}),
}

if value > 0:
post_state[query_contract] = Account(storage={0: 0}, balance=value)

blockchain_test(
pre=pre,
blocks=[block],
post=post_state,
)


def test_bal_4788_selfdestruct_to_beacon_root(
pre: Alloc,
blockchain_test: BlockchainTestFiller,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
| `test_bal_4788_simple` | Ensure BAL captures beacon root storage writes during pre-execution system call | Block with 2 normal user transactions: Alice sends 10 wei to Charlie, Bob sends 10 wei to Charlie. At block start (pre-execution), `SYSTEM_ADDRESS` calls `BEACON_ROOTS_ADDRESS` to store parent beacon root | BAL **MUST** include at `block_access_index=0`: `BEACON_ROOTS_ADDRESS` with two `storage_changes` (timestamp slot and beacon root slot); `SYSTEM_ADDRESS` **MUST NOT** be included in BAL. At `block_access_index=1`: Alice with `nonce_changes`, Charlie with `balance_changes` (10 wei). At `block_access_index=2`: Bob with `nonce_changes`, Charlie with `balance_changes` (20 wei total). | βœ… Completed |
| `test_bal_4788_empty_block` | Ensure BAL captures beacon root storage writes in empty block | Block with no transactions. At block start (pre-execution), `SYSTEM_ADDRESS` calls `BEACON_ROOTS_ADDRESS` to store parent beacon root | BAL **MUST** include at `block_access_index=0`: `BEACON_ROOTS_ADDRESS` with two `storage_changes` (timestamp slot and beacon root slot); `SYSTEM_ADDRESS` **MUST NOT** be included in BAL. No transaction-related BAL entries. | βœ… Completed |
| `test_bal_4788_query` | Ensure BAL captures storage reads when querying beacon root (valid and invalid queries) with optional value transfer | Parameterized test: Block 1 stores beacon root at timestamp 12. Block 2 queries with three timestamp scenarios (valid=12, invalid non-zero=42, invalid zero=0) and value (0 or 100 wei). Valid query (timestamp=12): reads both timestamp and root slots, writes returned value. If value > 0, beacon root contract receives balance. Invalid query with non-zero timestamp (timestamp=42): reads only timestamp slot before reverting, query contract has implicit SLOAD recorded (SSTORE reverts), no value transferred. Invalid query with zero timestamp (timestamp=0): reverts immediately without any storage access, query contract has implicit SLOAD recorded, no value transferred. | Block 1 BAL: System call writes. Block 2 BAL **MUST** include at `block_access_index=0`: System call writes for block 2. Valid case (timestamp=12) at `block_access_index=1`: `BEACON_ROOTS_ADDRESS` with `storage_reads` [timestamp_slot, root_slot] and `balance_changes` if value > 0, query contract with `storage_changes`. Invalid non-zero case (timestamp=42) at `block_access_index=1`: `BEACON_ROOTS_ADDRESS` with `storage_reads` [timestamp_slot only] and NO `balance_changes` (reverted), query contract with `storage_reads` [0] and NO `storage_changes`. Invalid zero case (timestamp=0) at `block_access_index=1`: `BEACON_ROOTS_ADDRESS` with NO `storage_reads` (reverts before access) and NO `balance_changes`, query contract with `storage_reads` [0] and NO `storage_changes`. | βœ… Completed |
| `test_bal_4788_invalid_calldata_size` | Ensure BAL correctly handles EIP-4788 queries with invalid calldata size | Parameterized test: Query contract calls `BEACON_ROOTS_ADDRESS` with variable-size calldata (0, 31, or 33 bytes) and value (0 or 100 wei). EIP-4788 requires exactly 32 bytes of calldata; any other size reverts before any storage access. | BAL **MUST** include at `block_access_index=0`: `BEACON_ROOTS_ADDRESS` with `storage_changes` from system call. At `block_access_index=1`: `BEACON_ROOTS_ADDRESS` with NO `storage_reads` (reverts before access) and NO `balance_changes` (value not transferred), query contract with `storage_reads` [0] (implicit from no-op SSTORE) and NO `storage_changes`. If value > 0, query contract retains balance. Alice with `nonce_changes`. | βœ… Completed |
| `test_bal_4788_selfdestruct_to_beacon_root` | Ensure BAL captures `SELFDESTRUCT` to beacon root address alongside system call storage writes | Single block: Pre-execution system call writes beacon root to storage. Transaction: Alice calls contract (pre-funded with 100 wei) that selfdestructs with `BEACON_ROOTS_ADDRESS` as beneficiary. | BAL **MUST** include at `block_access_index=0`: `BEACON_ROOTS_ADDRESS` with `storage_changes` (timestamp and root slots from system call). At `block_access_index=1`: Alice with `nonce_changes`, contract with `balance_changes` (100β†’0), `BEACON_ROOTS_ADDRESS` with `balance_changes` (receives 100 wei). | βœ… Completed |
| `test_bal_selfdestruct_send_to_sender` | Ensure BAL tracks SELFDESTRUCT sending all funds back to the tx sender (no burn) | Pre-state: contract `C` exists from a prior transaction with non-empty code and balance = 100 wei. EOA `Alice` sends a transaction calling `C`. `C`’s code executes `SELFDESTRUCT(Alice)`. Under EIP-6780, because `C` was not created in this transaction, SELFDESTRUCT does not delete code or storage; it only transfers the entire 100 wei balance from `C` to `Alice`. Final post-state: `C` still exists with the same code and balance = 0; `Alice`’s balance increased by 100 wei (ignoring gas for this test). | BAL **MUST** include `Alice` with `nonce_changes` (tx sender) and `balance_changes` reflecting receipt of 100 wei, and **MUST** include `C` with `balance_changes` 100β†’0 and no `code_changes`. BAL **MUST NOT** include any other accounts. This test ensures SELFDESTRUCT-to-sender is modeled as a pure value transfer (no burn, no code deletion). | 🟑 Planned |
| `test_bal_7002_clean_sweep` | Ensure BAL correctly tracks "clean sweep" where all withdrawal requests are dequeued in same block (requests ≀ MAX). Parameterized: (1) pubkey first 32 bytes zero / non-zero, (2) amount zero / non-zero | Alice sends transaction to `WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS` with 1 withdrawal request. Validator pubkey has either first 32 bytes zero or non-zero. Amount is either zero or non-zero. Since 1 ≀ MAX_WITHDRAWAL_REQUESTS_PER_BLOCK, post-execution system call dequeues all requests ("clean sweep"), resetting head and tail to 0. | BAL **MUST** include Alice with `nonce_changes` at `block_access_index=1`. `WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS` **MUST** have: `balance_changes` at `block_access_index=1` (receives fee), `storage_reads` for excess, head, and slot 5 (first 32 bytes of pubkey) if zero. At `block_access_index=1` (tx enqueue): `storage_changes` for count (0β†’1), tail (0β†’1), slot 4 (source address), slot 5 (first 32 bytes, **ONLY** if non-zero), slot 6. At `block_access_index=2` (post-exec dequeue): `storage_changes` for count (1β†’0), tail (1β†’0). Clean sweep invariant: when all requests dequeued, both head and tail reset to 0. | βœ… Completed |
Expand Down