@pytest.mark.parametrize("distinct_senders", [False, True])
@pytest.mark.parametrize("existing_slots", [False, True])
def test_sload_bloated_multi_contract(
benchmark_test: BenchmarkTestFiller,
pre: Alloc,
fork: Fork,
gas_benchmark_value: int,
tx_gas_limit: int,
existing_slots: bool,
distinct_senders: bool,
) -> None:
"""
Benchmark SLOAD across a distinct contract per transaction.
Each transaction calls a freshly-deployed contract whose slot 0
is pre-loaded with the starting offset; the contract then runs a
SLOAD loop over sequential slots until gas runs low. Unlike
test_sload_bloated_prefetch_miss which hammers one account's
storage trie via an EIP-7702 delegated EOA, every transaction
here opens a different storage trie, stressing cross-account
state access and state-trie breadth in a single block.
Every target contract first CALLs a shared offset_holder
contract whose slot 0 is read, incremented, and written back.
This mirrors the first test's "same-contract slot 0" dependency
pattern via cross-contract CALL: every transaction forms a
read-after-write edge on offset_holder's slot 0, preventing
parallel execution.
When ``distinct_senders`` is True every transaction uses a fresh
sender. This additionally exercises per-sender prewarm
serialization (e.g. Nethermind) differently than the shared-
sender case; we run both so clients can be measured in both
regimes.
"""
# Shared offset_holder: reads, increments, and writes its own
# slot 0. Every target CALLs this to create an inter-tx RAW
# dependency chain on a single shared storage slot.
offset_holder = pre.deploy_contract(
code=Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1)),
)
# Target runtime: CALL offset_holder (for the dependency), then
# run the same SLOAD loop as test_sload_bloated in its own
# storage. Final counter is written back to slot 0.
runtime_code = (
Op.POP(
Op.CALL(
address=offset_holder,
)
)
+ Op.SLOAD(Op.PUSH0)
+ While(
body=(Op.DUP1 + Op.SLOAD + Op.POP + Op.PUSH1(1) + Op.ADD),
condition=Op.GT(Op.GAS, LOOP_GAS_THRESHOLD),
)
+ Op.PUSH0
+ Op.SSTORE
)
base_offset = 1 if existing_slots else START_SLOT
max_sloads_per_tx = _max_sloads_per_tx(tx_gas_limit, fork)
# Pre-load slot 0 with the starting offset. For existing_slots,
# also fill the slot range the loop will read so SLOADs land on
# populated entries rather than empty slots. A fresh Storage
# instance is built per deployment (below) so that every target
# gets an independent root dict, not an alias of the same one.
storage_data: Storage.StorageDictType = {0: base_offset}
if existing_slots:
for i in range(base_offset, base_offset + max_sloads_per_tx):
storage_data[i] = i
intrinsic_gas = fork.transaction_intrinsic_cost_calculator()()
# Minimum per-tx gas ensuring the SLOAD loop runs at least one
# iteration so every target satisfies storage_reads=[base_offset]:
# intrinsic + CALL + offset_holder + setup + loop threshold
# + one iteration + final SSTORE, with buffer.
min_tx_gas = intrinsic_gas + 130_000
# 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
targets: list[Address] = []
txs: list[Transaction] = []
# Each tx targets a freshly-deployed contract with identical code
# and storage layout.
while gas_available >= min_tx_gas:
tx_gas = min(gas_available, tx_gas_limit)
target = pre.deploy_contract(
code=runtime_code,
storage=Storage(storage_data),
)
targets.append(target)
sender = next(senders_iter)
senders.append(sender)
txs.append(
Transaction(
gas_limit=tx_gas,
to=target,
sender=sender,
)
)
gas_available -= tx_gas
expectations: dict[Address, BalAccountExpectation] = {
offset_holder: BalAccountExpectation(
storage_changes=[
BalStorageSlot(
slot=0,
validate_any_change=True,
),
],
),
}
for t in targets:
expectations[t] = 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 = [
Block(
txs=txs,
expected_block_access_list=BlockAccessListExpectation(
account_expectations=expectations,
),
)
]
benchmark_test(
pre=pre,
blocks=blocks,
skip_gas_used_validation=True,
verifications=[ReceiptStatusExpected()],
)