@pytest.mark.parametrize("transfer_amount", [0, 1])
@pytest.mark.parametrize("opcode", [Op.CALL, Op.CALLCODE])
@pytest.mark.parametrize("access_warm", [True, False])
def test_contract_calling_many_addresses(
benchmark_test: BenchmarkTestFiller,
pre: Alloc,
fork: Fork,
transfer_amount: int,
opcode: Op,
access_warm: bool,
gas_benchmark_value: int,
fixed_opcode_count: float | None,
) -> None:
"""Benchmark a contract that calls many distinct addresses."""
start_addr = 2**80 - 1
value_transfer = transfer_amount > 0
# Only CALL creates accounts on value transfer (CALLCODE doesn't).
account_creation = value_transfer and opcode == Op.CALL
setup = (
Op.ADD(1, Op.CALLDATALOAD(32)) # [end+1 = limit]
+ Op.CALLDATALOAD(0) # [start = index, limit]
)
iterating = While(
body=Op.POP(
opcode(
address=Op.ADD(start_addr, Op.DUP6),
value=transfer_amount,
# gas accounting
address_warm=access_warm,
value_transfer=value_transfer,
account_new=account_creation,
)
),
condition=Op.PUSH1(1) # [1, index, limit]
+ Op.ADD # [index+1, limit]
+ Op.DUP1 # [index+1, index+1, limit]
+ Op.DUP3 # [limit, index+1, index+1, limit]
+ Op.GT, # [limit > index+1, index+1, limit]
)
code = IteratingBytecode(
setup=setup,
iterating=iterating,
cleanup=Op.STOP,
)
contract_address = pre.deploy_contract(
code=code,
balance=10**9 if value_transfer else 0,
)
def calldata(iteration_count: int, start_iteration: int) -> bytes:
index_end = start_iteration + iteration_count - 1
return Hash(start_iteration) + Hash(index_end)
def access_list(
iteration_count: int, start_iteration: int
) -> list[AccessList]:
return [
AccessList(address=Address(start_addr + i), storage_keys=[])
for i in range(start_iteration, start_iteration + iteration_count)
]
tx_kwargs: dict = {
"calldata": calldata,
"access_list": access_list if access_warm else None,
}
stipend = fork.call_value_stipend() if value_transfer else 0
def packing_budget() -> int:
"""
Return the gas budget to size the transactions against.
Every iteration is charged the call stipend but gets it back
unused, so the block consumes less than it is charged; raising the
budget by that ratio lands the block on the target. The stipend
only comes back out of the execution dimension, so a block bounded
by the state dimension needs no adjustment.
"""
state_bound = code.state_gas_cost_by_iteration_count(
fork=fork, iteration_count=1
)
if not stipend or state_bound:
return gas_benchmark_value
per_iteration = code.tx_execution_gas_cost_by_iteration_count(
fork=fork, iteration_count=2, **tx_kwargs
) - code.tx_execution_gas_cost_by_iteration_count(
fork=fork, iteration_count=1, **tx_kwargs
)
return gas_benchmark_value * per_iteration // (per_iteration - stipend)
if fixed_opcode_count is not None:
total_iterations = int(fixed_opcode_count * 1000)
else:
total_iterations = sum(
code.tx_iterations_by_gas_limit(
fork=fork, gas_limit=packing_budget(), **tx_kwargs
)
)
def block_gas(iterations: int) -> int:
"""Return the block gas the iterations land, per gas dimension."""
execution = 0
state = 0
start_iteration = 0
for iteration_count in code.tx_iterations_by_total_iteration_count(
fork=fork, total_iterations=iterations, **tx_kwargs
):
execution += code.tx_execution_gas_cost_by_iteration_count(
fork=fork,
iteration_count=iteration_count,
start_iteration=start_iteration,
**tx_kwargs,
)
state += code.state_gas_cost_by_iteration_count(
fork=fork, iteration_count=iteration_count
)
start_iteration += iteration_count
# The block header carries the larger dimension only (EIP-8037),
# and the unused stipend comes back out of the execution one.
return max(execution - stipend * iterations, state)
# The raised packing budget inflates the per-transaction intrinsic gas
# too, which earns no stipend back, so the block can overshoot the
# target by up to one iteration.
while (
total_iterations > 0
and block_gas(total_iterations) > gas_benchmark_value
):
total_iterations -= 1
if total_iterations == 0:
pytest.skip(
"Benchmark gas value cannot cover a single call to the contract."
)
with TestPhaseManager.execution():
sender = pre.fund_eoa()
exec_txs = list(
code.transactions_by_total_iteration_count(
fork=fork,
total_iterations=total_iterations,
sender=sender,
to=contract_address,
**tx_kwargs,
)
)
total_gas_cost = sum(tx.gas_cost for tx in exec_txs)
total_gas_cost -= stipend * total_iterations
post = {
Address(start_addr + i): Account(balance=transfer_amount)
for i in range(total_iterations)
if account_creation
}
benchmark_test(
post=post,
blocks=[Block(txs=exec_txs)],
expected_benchmark_gas_used=total_gas_cost,
)