@pytest.mark.stub_parametrize("token_name", "bloated_eoa_")
@pytest.mark.parametrize("distinct_senders", [False, True])
@pytest.mark.parametrize("existing_slots", [False, True])
def test_sload_bloated_prefetch_miss(
benchmark_test: BenchmarkTestFiller,
pre: Alloc,
fork: Fork,
gas_benchmark_value: int,
tx_gas_limit: int,
token_name: str,
existing_slots: bool,
distinct_senders: bool,
) -> None:
"""
Benchmark SLOAD with calldata-driven offsets to defeat prefetching.
A small first transaction writes an initial offset into the
authority's slot 0 via calldata. Subsequent max-gas transactions
each read the previous offset from slot 0, immediately overwrite
slot 0 with a new offset from their own calldata, then SLOAD
sequentially from the previous offset. Because each transaction's
SLOAD range depends on state written by its predecessor, a
prefetcher that predicts SLOAD targets from pre-block state
without simulating intra-block writes will pre-warm incorrect
storage slots. The minimal first tx is load-bearing: it lives
inside the benchmark block so every subsequent max-gas tx reads
a slot 0 value that differs from the prefetcher's pre-block
snapshot, achieving a 100% miss rate.
When ``distinct_senders`` is True every transaction uses a fresh
sender. This additionally defeats per-sender prewarm
serialization (e.g. Nethermind) that groups txs by sender and
runs them sequentially to propagate state changes — forcing
every tx's prewarm scope to restart from pre-block state.
"""
plant_code = Op.SLOAD(Op.PUSH0, key_warm=False) + Op.SSTORE(
Op.PUSH0,
Op.CALLDATALOAD(Op.PUSH0),
key_warm=True,
original_value=0,
current_value=0,
new_value=1,
)
# Read the old offset from slot 0, write the new offset from
# calldata to slot 0, then SLOAD sequentially from the old offset.
runtime_code = plant_code + While(
body=(Op.DUP1 + Op.SLOAD + Op.POP + Op.PUSH1(1) + Op.ADD),
condition=Op.GT(Op.GAS, LOOP_GAS_THRESHOLD),
)
authority = pre.stub_eoa(token_name)
runtime_address = pre.deploy_contract(code=runtime_code)
# Setup: delegate authority to the runtime contract. Slot 0 is
# left at 0 (the delegation tx's calldata) so the benchmark
# block's pre-state has slot 0 = 0; the first benchmark tx
# then plants base_offset in slot 0 inside the benchmark block,
# forcing the prefetcher's pre-block snapshot to disagree with
# the actual slot 0 value seen by every max-gas tx that follows.
delegation_tx = delegate_with_calldata(
pre,
fork,
authority,
runtime_address,
Hash(0),
)
blocks: list[Block] = [Block(txs=[delegation_tx])]
# Offset spacing: upper bound on SLOADs per tx ensures each
# transaction reads a completely disjoint slot range.
max_sloads_per_tx = _max_sloads_per_tx(tx_gas_limit, fork)
# The base offset must be at least max_sloads_per_tx away from
# the pre-block slot 0 value (0) so the prefetcher's predicted
# SLOAD range is completely disjoint from the actual range.
base_offset = max_sloads_per_tx if existing_slots else START_SLOT
intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(
calldata=Hash(base_offset),
recipient_type=RecipientType.DELEGATION_7702,
return_cost_deducted_prior_execution=True,
) + fork.transaction_top_frame_gas_calculator()(
recipient_type=RecipientType.DELEGATION_7702
)
plant_tx_gas = (
intrinsic_gas + plant_code.gas_cost(fork) + LOOP_GAS_THRESHOLD
)
# senders_iter yields one sender per tx (fresh per call in
# distinct mode, a single shared sender otherwise). The senders
# list collects one entry per tx so the BAL builder below can
# group nonce changes by sender uniformly.
senders_iter = _sender_generator(pre, distinct_senders)
senders: list[EOA] = []
gas_available = gas_benchmark_value
txs: list[Transaction] = []
# This tx's job is to change slot 0 inside the benchmark block so
# every subsequent max-gas tx reads an offset the prefetcher's
# pre-block snapshot does not see, achieving a 100% miss rate.
first_tx_gas = min(gas_available, plant_tx_gas)
sender = next(senders_iter)
senders.append(sender)
txs.append(
Transaction(
gas_limit=first_tx_gas,
to=authority,
data=Hash(base_offset),
sender=sender,
)
)
gas_available -= first_tx_gas
# Subsequent transactions: max gas, each shifts the offset
# so the next transaction SLOADs from a different range.
tx_index = 1
while gas_available >= plant_tx_gas:
tx_gas = min(gas_available, tx_gas_limit)
new_offset = base_offset + tx_index * max_sloads_per_tx
sender = next(senders_iter)
senders.append(sender)
txs.append(
Transaction(
gas_limit=tx_gas,
to=authority,
data=Hash(new_offset),
sender=sender,
)
)
gas_available -= tx_gas
tx_index += 1
expectations: dict[Address, BalAccountExpectation] = {
authority: BalAccountExpectation(
storage_reads=[base_offset],
storage_changes=[
BalStorageSlot(
slot=0,
validate_any_change=True,
),
],
),
}
sender_nonces: dict[Address, list[BalNonceChange]] = {}
for i, s in enumerate(senders):
changes = sender_nonces.setdefault(s, [])
changes.append(
BalNonceChange(
block_access_index=i + 1,
post_nonce=len(changes) + 1,
)
)
for addr, nonces in sender_nonces.items():
expectations[addr] = BalAccountExpectation(nonce_changes=nonces)
blocks.append(
Block(
txs=txs,
expected_block_access_list=BlockAccessListExpectation(
account_expectations=expectations,
),
)
)
benchmark_test(
pre=pre,
blocks=blocks,
skip_gas_used_validation=True,
verifications=[ReceiptStatusExpected()],
)