Skip to content

test_worst_mod()

Documentation for tests/benchmark/test_worst_compute.py::test_worst_mod@v5.1.0.

Generate fixtures for these test cases for Osaka with:

fill -v tests/benchmark/test_worst_compute.py::test_worst_mod -m benchmark

Test running a block with as many MOD instructions with arguments of the parametrized range. The test program consists of code segments evaluating the "MOD chain": mod[0] = calldataload(0) mod[1] = numerators[indexes[0]] % mod[0] mod[2] = numerators[indexes[1]] % mod[1] ... The "numerators" is a pool of 15 constants pushed to the EVM stack at the program start. 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/test_worst_compute.py
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
@pytest.mark.parametrize("mod_bits", [255, 191, 127, 63])
@pytest.mark.parametrize("op", [Op.MOD, Op.SMOD])
def test_worst_mod(
    state_test: StateTestFiller,
    pre: Alloc,
    fork: Fork,
    mod_bits: int,
    op: Op,
    gas_benchmark_value: int,
):
    """
    Test running a block with as many MOD instructions with arguments of the parametrized range.
    The test program consists of code segments evaluating the "MOD chain":
    mod[0] = calldataload(0)
    mod[1] = numerators[indexes[0]] % mod[0]
    mod[2] = numerators[indexes[1]] % mod[1] ...
    The "numerators" is a pool of 15 constants pushed to the EVM stack at the program start.
    The order of accessing the numerators is selected in a way the mod value remains in the range
    as long as possible.
    """
    max_code_size = fork.max_code_size()

    # For SMOD we negate both numerator and modulus. The underlying computation is the same,
    # just the SMOD implementation will have to additionally handle the sign bits.
    # The result stays negative.
    should_negate = op == Op.SMOD

    num_numerators = 15
    numerator_bits = 256 if not should_negate else 255
    numerator_max = 2**numerator_bits - 1
    numerator_min = 2 ** (numerator_bits - 1)

    # 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 MOD chain.
    # You can look for a longer one by increasing the numerators_min_len. This will activate
    # the while loop below.
    match op, mod_bits:
        case Op.MOD, 255:
            seed = 20393
            numerators_min_len = 750
        case Op.MOD, 191:
            seed = 25979
            numerators_min_len = 770
        case Op.MOD, 127:
            seed = 17671
            numerators_min_len = 750
        case Op.MOD, 63:
            seed = 29181
            numerators_min_len = 730
        case Op.SMOD, 255:
            seed = 4015
            numerators_min_len = 750
        case Op.SMOD, 191:
            seed = 17355
            numerators_min_len = 750
        case Op.SMOD, 127:
            seed = 897
            numerators_min_len = 750
        case Op.SMOD, 63:
            seed = 7562
            numerators_min_len = 720
        case _:
            raise ValueError(f"{mod_bits}-bit {op} not supported.")

    while True:
        rng = random.Random(seed)

        # Create the list of random numerators.
        numerators = [rng.randint(numerator_min, numerator_max) for _ in range(num_numerators)]

        # Create the random initial modulus.
        initial_mod = rng.randint(2 ** (mod_bits - 1), 2**mod_bits - 1)

        # Evaluate the MOD chain and collect the order of accessing numerators.
        mod = initial_mod
        indexes = []
        while mod >= mod_min:
            results = [n % mod for n in numerators]  # Compute results for each numerator.
            i = max(range(len(results)), key=results.__getitem__)  # And pick the best one.
            mod = results[i]
            indexes.append(i)

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

    # TODO: Don't use fixed PUSH32. Let Bytecode helpers to select optimal push opcode.
    code_constant_pool = sum((Op.PUSH32[n] for n in numerators), Bytecode())
    code_prefix = code_constant_pool + Op.JUMPDEST
    code_suffix = Op.JUMP(len(code_constant_pool))
    code_body_len = max_code_size - len(code_prefix) - len(code_suffix)
    code_segment = (
        Op.CALLDATALOAD(0) + sum(make_dup(len(numerators) - i) + op for i in indexes) + Op.POP
    )
    code = (
        code_prefix
        # TODO: Add int * Bytecode support
        + sum(code_segment for _ in range(code_body_len // len(code_segment)))
        + code_suffix
    )
    assert (max_code_size - len(code_segment)) < len(code) <= max_code_size

    input_value = initial_mod if not should_negate else neg(initial_mod)
    tx = Transaction(
        to=pre.deploy_contract(code=code),
        data=input_value.to_bytes(32, byteorder="big"),
        gas_limit=gas_benchmark_value,
        sender=pre.fund_eoa(),
    )

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

Parametrized Test Cases

The interactive table below is also available as a standalone page.

Test ID (Abbreviated) op mod_bits
...fork_Prague-state_test-op_MOD-mod_bits_255 MOD 255
...fork_Prague-state_test-op_MOD-mod_bits_191 MOD 191
...fork_Prague-state_test-op_MOD-mod_bits_127 MOD 127
...fork_Prague-state_test-op_MOD-mod_bits_63 MOD 63
...fork_Prague-state_test-op_SMOD-mod_bits_255 SMOD 255
...fork_Prague-state_test-op_SMOD-mod_bits_191 SMOD 191
...fork_Prague-state_test-op_SMOD-mod_bits_127 SMOD 127
...fork_Prague-state_test-op_SMOD-mod_bits_63 SMOD 63
...fork_Prague-blockchain_test_from_state_test-op_MOD-mod_bits_255 MOD 255
...fork_Prague-blockchain_test_from_state_test-op_MOD-mod_bits_191 MOD 191
...fork_Prague-blockchain_test_from_state_test-op_MOD-mod_bits_127 MOD 127
...fork_Prague-blockchain_test_from_state_test-op_MOD-mod_bits_63 MOD 63
...fork_Prague-blockchain_test_from_state_test-op_SMOD-mod_bits_255 SMOD 255
...fork_Prague-blockchain_test_from_state_test-op_SMOD-mod_bits_191 SMOD 191
...fork_Prague-blockchain_test_from_state_test-op_SMOD-mod_bits_127 SMOD 127
...fork_Prague-blockchain_test_from_state_test-op_SMOD-mod_bits_63 SMOD 63
...fork_Osaka-state_test-op_MOD-mod_bits_255 MOD 255
...fork_Osaka-state_test-op_MOD-mod_bits_191 MOD 191
...fork_Osaka-state_test-op_MOD-mod_bits_127 MOD 127
...fork_Osaka-state_test-op_MOD-mod_bits_63 MOD 63
...fork_Osaka-state_test-op_SMOD-mod_bits_255 SMOD 255
...fork_Osaka-state_test-op_SMOD-mod_bits_191 SMOD 191
...fork_Osaka-state_test-op_SMOD-mod_bits_127 SMOD 127
...fork_Osaka-state_test-op_SMOD-mod_bits_63 SMOD 63
...fork_Osaka-blockchain_test_from_state_test-op_MOD-mod_bits_255 MOD 255
...fork_Osaka-blockchain_test_from_state_test-op_MOD-mod_bits_191 MOD 191
...fork_Osaka-blockchain_test_from_state_test-op_MOD-mod_bits_127 MOD 127
...fork_Osaka-blockchain_test_from_state_test-op_MOD-mod_bits_63 MOD 63
...fork_Osaka-blockchain_test_from_state_test-op_SMOD-mod_bits_255 SMOD 255
...fork_Osaka-blockchain_test_from_state_test-op_SMOD-mod_bits_191 SMOD 191
...fork_Osaka-blockchain_test_from_state_test-op_SMOD-mod_bits_127 SMOD 127
...fork_Osaka-blockchain_test_from_state_test-op_SMOD-mod_bits_63 SMOD 63