@pytest.mark.repricing(absent_accounts=True)
@pytest.mark.parametrize(
"opcode",
[
Op.BALANCE,
Op.EXTCODESIZE,
Op.EXTCODEHASH,
],
)
@pytest.mark.parametrize(
"absent_accounts",
[
True,
False,
],
)
def test_ext_account_query_cold(
benchmark_test: BenchmarkTestFiller,
pre: Alloc,
fork: Fork,
opcode: Op,
absent_accounts: bool,
gas_benchmark_value: int,
tx_gas_limit: int,
fixed_opcode_count: int,
) -> None:
"""
Benchmark stateful opcodes accessing cold accounts.
"""
if fixed_opcode_count:
pytest.skip(
"Cold-account queries need one unique account per opcode, "
"so the iteration count is gas-driven, not fixed"
)
attack_gas_limit = gas_benchmark_value
intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator()
cold_account_access_gas = opcode(
0,
# gas accounting
address_warm=False,
).gas_cost(fork)
# For calculation robustness, the calculation below ignores "glue" opcodes
# like PUSH and POP. It should be considered a worst-case number of
# accounts, and a few of them might not be targeted before the attacking
# transaction runs out of gas.
num_target_accounts = (
attack_gas_limit - intrinsic_gas_cost_calc()
) // cold_account_access_gas
blocks = []
post = {}
# Setup The target addresses are going to be constructed (in the case of
# absent=False) and called as addr_offset + i, where i is the index of the
# account. This is to avoid collisions with the addresses indirectly
# created by the testing framework.
addr_offset = int.from_bytes(pre.fund_eoa(amount=0))
if not absent_accounts:
account_creation_gas = Op.CALL(
value=1, address_warm=False, account_new=True, value_transfer=True
).gas_cost(fork)
# To avoid brittle/tight gas calculations of glue opcodes, we take
# 90% of the maximum tx capacity. Even if this calculation fails
# in the future, it will be caught by the post-state check.
# Also, this is only for the setup phase, so being optimal is
# not critical.
max_creations_per_tx = int(
(tx_gas_limit * 0.9) // account_creation_gas
)
factory_code = (
Op.CALLDATALOAD(0) # addr_start
+ Op.PUSH4(max_creations_per_tx) # counter
# Stack: [counter, addr_start]
+ While(
body=Op.POP(
Op.CALL(
address=Op.ADD(addr_offset, Op.ADD(Op.DUP7, Op.DUP7)),
value=10,
)
),
condition=Op.PUSH1(1)
+ Op.SWAP1
+ Op.SUB
+ Op.DUP1
+ Op.ISZERO
+ Op.ISZERO,
)
)
factory_address = pre.deploy_contract(
code=factory_code, balance=10**18
)
creation_txs = []
with TestPhaseManager.setup():
num_creation_txs = math.ceil(
num_target_accounts / max_creations_per_tx
)
for i in range(num_creation_txs):
addr_start = i * int(max_creations_per_tx)
creation_txs.append(
Transaction(
to=factory_address,
data=Hash(addr_start),
gas_limit=tx_gas_limit,
sender=pre.fund_eoa(),
)
)
blocks.append(Block(txs=creation_txs))
for i in range(num_target_accounts):
addr = Address(i + addr_offset + 1)
post[addr] = Account(balance=10)
# Execution
op_code = (
Op.CALLDATALOAD(0) # address_start
+ Op.CALLDATALOAD(32) # num_to_query
# Stack: [num_to_query, address_start]
+ While(
body=Op.POP(opcode(Op.ADD(addr_offset, Op.ADD(Op.DUP2, Op.DUP2)))),
condition=Op.PUSH1(1)
+ Op.SWAP1
+ Op.SUB
+ Op.DUP1
+ Op.ISZERO
+ Op.ISZERO,
)
)
op_address = pre.deploy_contract(code=op_code)
execution_txs = []
with TestPhaseManager.execution():
max_target_per_tx = (
tx_gas_limit - intrinsic_gas_cost_calc()
) // cold_account_access_gas
num_execution_txs = math.ceil(num_target_accounts / max_target_per_tx)
gas_used = 0
for i in range(num_execution_txs):
address_start = i * int(max_target_per_tx)
remaining = num_target_accounts - address_start
num_to_query = min(int(max_target_per_tx), remaining)
gas_limit = min(tx_gas_limit, attack_gas_limit - gas_used)
calldata = Hash(address_start) + Hash(num_to_query)
if gas_limit < intrinsic_gas_cost_calc(calldata=calldata):
break
execution_txs.append(
Transaction(
to=op_address,
data=calldata,
gas_limit=gas_limit,
sender=pre.fund_eoa(),
)
)
gas_used += gas_limit
blocks.append(Block(txs=execution_txs))
benchmark_test(
target_opcode=opcode,
post=post,
blocks=blocks,
)