Skip to content

test_ext_account_query_cold()

Documentation for tests/benchmark/compute/instruction/test_account_query.py::test_ext_account_query_cold@ffe48572.

Generate fixtures for these test cases for Bogota with:

fill -v tests/benchmark/compute/instruction/test_account_query.py::test_ext_account_query_cold --gas-benchmark-values 1

Benchmark stateful opcodes accessing cold accounts.

Source code in tests/benchmark/compute/instruction/test_account_query.py
@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,
    )

Parametrized Test Cases

This test generates 6 parametrized test cases across 4 forks.