Skip to content

test_repeated_underfunded_calls()

Documentation for tests/frontier/precompiles/test_ecrecover.py::test_repeated_underfunded_calls@ffe48572.

Generate fixtures for these test cases for Bogota with:

fill -v tests/frontier/precompiles/test_ecrecover.py::test_repeated_underfunded_calls --fork Bogota

Test that repeated underfunded calls to ecrecover all fail, leave the return buffer untouched and move no value to the precompile.

Source code in tests/frontier/precompiles/test_ecrecover.py
@pytest.mark.ported_from(
    [
        "state_tests/stQuadraticComplexityTest/Call50000_ecrecFiller.json",
        "state_tests/stStaticCall/static_Call50000_ecrecFiller.json",
    ],
    coverage_missed_reason=(
        "The fillers repeat the call until the transaction runs out of gas, "
        "so their whole post-state is the empty one a reverted transaction "
        "leaves behind. The port runs its iterations to completion and "
        "checks the residue instead, keeping the oversized input window but "
        "not the cost of the repetition itself."
    ),
)
@pytest.mark.with_all_call_opcodes
@pytest.mark.valid_from("Frontier")
def test_repeated_underfunded_calls(
    state_test: StateTestFiller,
    pre: Alloc,
    fork: Fork,
    call_opcode: Op,
) -> None:
    """
    Test that repeated underfunded calls to ecrecover all fail, leave the
    return buffer untouched and move no value to the precompile.
    """
    iterations = 10

    # The same signature recovers an address in `test_precompiles`, so the
    # gas is the only reason these calls fail.
    signature = EcrecoverInput(
        msg_hash=(
            0x18C547E4F7B0F325AD1E56F57E26C745B09A3E503D86E00E5255FF7F715D3D1C
        ),
        v=0x1C,
        r=0x73B1693892219D736CABA55BDB67216E485557EA6B6AF75F37096C9AA6A5A75F,
        s=0xEEB940B1D03B21E36B0E47E79769F095FE2AB855BD91E3A38756B7D75A9C4549,
    )

    # A value-bearing call adds the stipend to what the callee receives, so
    # the operand that leaves the precompile one gas short depends on the
    # opcode.
    gas_costs = fork.gas_costs()
    sends_value = "value" in call_opcode.kwargs
    stipend = gas_costs.CALL_STIPEND if sends_value else 0
    forwarded_gas = gas_costs.PRECOMPILE_ECRECOVER - stipend - 1

    # Memory: the input window, then the return buffer, then the loop
    # counter. The window is far wider than the 128 bytes the precompile
    # reads, as in the fillers: a client that copies the whole window
    # before charging for the call pays for it once per iteration.
    args_size = 50_000
    ret_offset = args_size
    counter_offset = ret_offset + 32

    # A failed call returns no bytes, so both seeded values must survive.
    successes_base = 0xC0DE
    ret_sentinel = b"\xff" * 32

    storage = Storage()
    successes_slot = storage.store_next(successes_base, "successes")

    # One wei per iteration, so no call can fail for want of balance.
    caller_balance = iterations
    value_kwarg: dict[str, Any] = {"value": 1} if sends_value else {}
    call = call_opcode(
        gas=forwarded_gas,
        address=Spec.ECRECOVER,
        args_offset=0,
        args_size=args_size,
        ret_offset=ret_offset,
        ret_size=32,
        **value_kwarg,
    )
    body = Op.SSTORE(
        successes_slot, Op.ADD(Op.SLOAD(successes_slot), call)
    ) + Op.MSTORE(counter_offset, Op.ADD(Op.MLOAD(counter_offset), 1))

    caller = pre.deploy_contract(
        Op.CALLDATACOPY(0, 0, len(bytes(signature)))
        + Op.MSTORE(ret_offset, ret_sentinel)
        + While(
            body=body,
            condition=Op.LT(Op.MLOAD(counter_offset), iterations),
        )
        + Op.SSTORE(storage.store_next(iterations), Op.MLOAD(counter_offset))
        + Op.SSTORE(storage.store_next(ret_sentinel), Op.MLOAD(ret_offset))
        + Op.STOP,
        storage={successes_slot: successes_base},
        balance=caller_balance,
    )

    tx = Transaction(
        to=caller,
        data=signature,
        sender=pre.fund_eoa(),
        protected=fork.supports_protected_txs(),
    )

    post = {
        caller: Account(storage=storage, balance=caller_balance),
        Spec.ECRECOVER: Account.NONEXISTENT,
    }

    state_test(pre=pre, post=post, tx=tx)

Parametrized Test Cases

This test generates 4 parametrized test cases across 17 forks.