diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py index c3c0c5e5b9c..107cf428124 100644 --- a/tests/zkevm/test_worst_compute.py +++ b/tests/zkevm/test_worst_compute.py @@ -103,3 +103,70 @@ def test_worst_keccak( post={}, blocks=[Block(txs=[tx])], ) + + +@pytest.mark.zkevm +@pytest.mark.valid_from("Cancun") +@pytest.mark.parametrize( + "gas_limit", + [ + Environment().gas_limit, + ], +) +def test_worst_modexp( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + gas_limit: int, +): + """Test running a block with as many MODEXP calls as possible.""" + env = Environment(gas_limit=gas_limit) + + base_mod_length = 32 + exp_length = 32 + + base = 2 ** (8 * base_mod_length) - 1 + mod = 2 ** (8 * base_mod_length) - 2 # Prevents base == mod + exp = 2 ** (8 * exp_length) - 1 + + # MODEXP calldata + calldata = ( + Op.MSTORE(0 * 32, base_mod_length) + + Op.MSTORE(1 * 32, exp_length) + + Op.MSTORE(2 * 32, base_mod_length) + + Op.MSTORE(3 * 32, base) + + Op.MSTORE(4 * 32, exp) + + Op.MSTORE(5 * 32, mod) + ) + + # EIP-2565 + mul_complexity = math.ceil(base_mod_length / 8) ** 2 + iter_complexity = exp.bit_length() - 1 + gas_cost = math.floor((mul_complexity * iter_complexity) / 3) + attack_block = Op.POP(Op.STATICCALL(gas_cost, 0x5, 0, 32 * 6, 0, 0)) + + # The attack contract is: JUMPDEST + [attack_block]* + PUSH0 + JUMP + jumpdest = Op.JUMPDEST + jump_back = Op.JUMP(len(calldata)) + max_iters_loop = (MAX_CODE_SIZE - len(calldata) - len(jumpdest) - len(jump_back)) // len( + attack_block + ) + code = calldata + jumpdest + sum([attack_block] * max_iters_loop) + jump_back + if len(code) > MAX_CODE_SIZE: + # Must never happen, but keep it as a sanity check. + raise ValueError(f"Code size {len(code)} exceeds maximum code size {MAX_CODE_SIZE}") + + code_address = pre.deploy_contract(code=code) + + tx = Transaction( + to=code_address, + gas_limit=gas_limit, + sender=pre.fund_eoa(), + ) + + blockchain_test( + env=env, + pre=pre, + post={}, + blocks=[Block(txs=[tx])], + )