Skip to content

test_mod_arithmetic()

Documentation for tests/benchmark/compute/instruction/test_arithmetic.py::test_mod_arithmetic@ffe48572.

Generate fixtures for these test cases for Bogota with:

fill -v tests/benchmark/compute/instruction/test_arithmetic.py::test_mod_arithmetic --gas-benchmark-values 1

Benchmark ADDMOD and MULMOD instructions.

The program consists of code segments evaluating the "op chain": mod[0] = calldataload(0) mod[1] = (fixed_arg op args[indexes[0]]) % mod[0] mod[2] = (fixed_arg op args[indexes[1]]) % mod[1] The "args" is a pool of 15 constants pushed to the EVM stack at the program start. The "fixed_arg" is the 0xFF...FF constant added to the EVM stack by PUSH32 just before executing the "op". The order of accessing the numerators is selected in a way the mod value remains in the range as long as possible.

Source code in tests/benchmark/compute/instruction/test_arithmetic.py
@pytest.mark.repricing(mod_bits=191)
@pytest.mark.parametrize("mod_bits", [255, 191, 127, 63])
@pytest.mark.parametrize("opcode", [Op.ADDMOD, Op.MULMOD])
def test_mod_arithmetic(
    benchmark_test: BenchmarkTestFiller,
    pre: Alloc,
    fork: Fork,
    mod_bits: int,
    opcode: Op,
    gas_benchmark_value: int,
) -> None:
    """
    Benchmark ADDMOD and MULMOD instructions.

    The program consists of code segments evaluating the "op chain":
    mod[0] = calldataload(0)
    mod[1] = (fixed_arg op args[indexes[0]]) % mod[0]
    mod[2] = (fixed_arg op args[indexes[1]]) % mod[1]
    The "args" is a pool of 15 constants pushed to the EVM stack at the program
    start.
    The "fixed_arg" is the 0xFF...FF constant added to the EVM stack by PUSH32
    just before executing the "op".
    The order of accessing the numerators is selected in a way the mod value
    remains in the range as long as possible.
    """
    fixed_arg = 2**256 - 1
    num_args = 15

    max_code_size = fork.max_code_size()

    # Pick the modulus min value so that it is _unlikely_ to drop to the lower
    # word count.
    assert mod_bits >= 63
    mod_min = 2 ** (mod_bits - 63)

    # Select the random seed giving the longest found op chain. You can look
    # for a longer one by increasing the op_chain_len. This will activate the
    # while loop below.
    op_chain_len = 666
    match opcode, mod_bits:
        case Op.ADDMOD, 255:
            seed = 4
        case Op.ADDMOD, 191:
            seed = 2
        case Op.ADDMOD, 127:
            seed = 2
        case Op.ADDMOD, 63:
            seed = 64
        case Op.MULMOD, 255:
            seed = 5
        case Op.MULMOD, 191:
            seed = 389
        case Op.MULMOD, 127:
            seed = 5
        case Op.MULMOD, 63:
            # For this setup we were not able to find an op-chain longer than
            # 600.
            seed = 4193
            op_chain_len = 600
        case _:
            raise ValueError(f"{mod_bits}-bit {opcode} not supported.")

    while True:
        rng = random.Random(seed)
        args = [rng.randint(2**255, 2**256 - 1) for _ in range(num_args)]
        initial_mod = rng.randint(2 ** (mod_bits - 1), 2**mod_bits - 1)

        # Evaluate the op chain and collect the order of accessing numerators.
        op_fn = operator.add if opcode == Op.ADDMOD else operator.mul
        mod = initial_mod
        indexes: list[int] = []
        while mod >= mod_min and len(indexes) < op_chain_len:
            results = [op_fn(a, fixed_arg) % mod for a in args]
            # And pick the best one.
            i = max(range(len(results)), key=results.__getitem__)
            mod = results[i]
            indexes.append(i)

        # Disable if you want to find longer op chains.
        assert len(indexes) == op_chain_len
        if len(indexes) == op_chain_len:
            break
        seed += 1
        print(f"{seed=}")

    code_constant_pool = sum((Op.PUSH32[n] for n in args), Bytecode())
    code_segment = (
        Op.CALLDATALOAD(0)
        + sum(
            make_dup(len(args) - i) + Op.PUSH32[fixed_arg] + opcode
            for i in indexes
        )
        + Op.POP
    )

    code_prefix = code_constant_pool + Op.JUMPDEST
    code_suffix = Op.JUMP(len(code_constant_pool))
    overhead = len(code_prefix) + len(code_suffix)
    num_segments = (max_code_size - overhead) // len(code_segment)
    code = code_prefix + code_segment * num_segments + code_suffix
    assert len(code) <= max_code_size

    tx = Transaction(
        to=pre.deploy_contract(code=code),
        data=initial_mod.to_bytes(32, byteorder="big"),
        gas_limit=gas_benchmark_value,
        sender=pre.fund_eoa(),
    )

    benchmark_test(
        tx=tx,
    )

Parametrized Test Cases

This test generates 8 parametrized test cases across 4 forks.