|
| 1 | +""" |
| 2 | +abstract: Tests for zkEVMs |
| 3 | + Tests for zkEVMs. |
| 4 | +
|
| 5 | +Tests for zkEVMs worst-cases scenarios. |
| 6 | +""" |
| 7 | + |
| 8 | +import pytest |
| 9 | + |
| 10 | +from ethereum_test_forks import Fork |
| 11 | +from ethereum_test_tools import ( |
| 12 | + Account, |
| 13 | + Alloc, |
| 14 | + Block, |
| 15 | + BlockchainTestFiller, |
| 16 | + Environment, |
| 17 | + Hash, |
| 18 | + Transaction, |
| 19 | + While, |
| 20 | + compute_create_address, |
| 21 | +) |
| 22 | +from ethereum_test_tools.vm.opcode import Opcodes as Op |
| 23 | + |
| 24 | +REFERENCE_SPEC_GIT_PATH = "TODO" |
| 25 | +REFERENCE_SPEC_VERSION = "TODO" |
| 26 | + |
| 27 | +MAX_CONTRACT_SIZE = 24 * 1024 # TODO: This could be a fork property |
| 28 | +BLOCK_GAS_LIMIT = 36_000_000 # TODO: Parametrize using the (yet to be implemented) block gas limit |
| 29 | +# OPCODE_GAS_LIMIT = BLOCK_GAS_LIMIT # TODO: Reduced in order to run the test in a reasonable time |
| 30 | +OPCODE_GAS_LIMIT = 100_000 |
| 31 | + |
| 32 | +XOR_TABLE_SIZE = 256 |
| 33 | +XOR_TABLE = [Hash(i).sha256() for i in range(XOR_TABLE_SIZE)] |
| 34 | + |
| 35 | + |
| 36 | +# TODO: Parametrize for EOF |
| 37 | +@pytest.mark.zkevm |
| 38 | +@pytest.mark.parametrize( |
| 39 | + "opcode", |
| 40 | + [ |
| 41 | + Op.EXTCODESIZE, |
| 42 | + ], |
| 43 | +) |
| 44 | +@pytest.mark.valid_from("Cancun") |
| 45 | +def test_worst_bytecode_single_opcode( |
| 46 | + blockchain_test: BlockchainTestFiller, |
| 47 | + pre: Alloc, |
| 48 | + fork: Fork, |
| 49 | + opcode: Op, |
| 50 | +): |
| 51 | + """ |
| 52 | + Test a block execution where a single opcode execution maxes out the gas limit, |
| 53 | + and the opcodes access a huge amount of contract code. |
| 54 | +
|
| 55 | + We first use a single block to deploy a factory contract that will be used to deploy |
| 56 | + a large number of contracts. |
| 57 | +
|
| 58 | + This is done to avoid having a big pre-allocation size for the test. |
| 59 | +
|
| 60 | + The test is performed in the last block of the test, and the entire block gas limit is |
| 61 | + consumed by repeated opcode executions. |
| 62 | + """ |
| 63 | + env = Environment(gas_limit=BLOCK_GAS_LIMIT) |
| 64 | + |
| 65 | + # The initcode will take its address as a starting point to the input to the keccak |
| 66 | + # hash function. |
| 67 | + # It will reuse the output of the hash function in a loop to create a large amount of |
| 68 | + # seemingly random code, until it reaches the maximum contract size. |
| 69 | + initcode = ( |
| 70 | + Op.MSTORE(0, Op.ADDRESS) |
| 71 | + + While( |
| 72 | + body=( |
| 73 | + Op.SHA3(Op.SUB(Op.MSIZE, 32), 32) |
| 74 | + # Use a xor table to avoid having to call the "expensive" sha3 opcode as much |
| 75 | + + sum( |
| 76 | + (Op.PUSH32[xor_value] + Op.XOR + Op.DUP1 + Op.MSIZE + Op.MSTORE) |
| 77 | + for xor_value in XOR_TABLE |
| 78 | + ) |
| 79 | + + Op.POP |
| 80 | + ), |
| 81 | + condition=Op.LT(Op.MSIZE, MAX_CONTRACT_SIZE), |
| 82 | + ) |
| 83 | + + Op.RETURN(0, MAX_CONTRACT_SIZE) |
| 84 | + ) |
| 85 | + initcode_address = pre.deploy_contract(code=initcode) |
| 86 | + |
| 87 | + # The factory contract will simply use the initcode that is already deployed, |
| 88 | + # and create a new contract and return its address if successful. |
| 89 | + factory_code = ( |
| 90 | + Op.EXTCODECOPY( |
| 91 | + address=initcode_address, |
| 92 | + dest_offset=0, |
| 93 | + offset=0, |
| 94 | + size=Op.EXTCODESIZE(initcode_address), |
| 95 | + ) |
| 96 | + + Op.MSTORE( |
| 97 | + 0, |
| 98 | + Op.CREATE( |
| 99 | + value=0, |
| 100 | + offset=0, |
| 101 | + size=Op.MSIZE, |
| 102 | + ), |
| 103 | + ) |
| 104 | + + Op.RETURN(0, 32) |
| 105 | + ) |
| 106 | + factory_address = pre.deploy_contract(code=factory_code) |
| 107 | + |
| 108 | + # The factory caller will call the factory contract N times, creating N new contracts. |
| 109 | + # Calldata should contain the N value. |
| 110 | + factory_caller_code = Op.CALLDATALOAD(0) + While( |
| 111 | + body=Op.POP(Op.CALL(address=factory_address)), |
| 112 | + condition=Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO, |
| 113 | + ) |
| 114 | + factory_caller_address = pre.deploy_contract(code=factory_caller_code) |
| 115 | + |
| 116 | + gas_costs = fork.gas_costs() |
| 117 | + intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator() |
| 118 | + max_number_of_contract_calls = (OPCODE_GAS_LIMIT - intrinsic_gas_cost_calc()) // ( |
| 119 | + gas_costs.G_VERY_LOW + gas_costs.G_BASE + gas_costs.G_COLD_ACCOUNT_ACCESS |
| 120 | + ) |
| 121 | + total_contracts_to_deploy = max_number_of_contract_calls |
| 122 | + approximate_gas_per_deployment = 4_970_000 # Obtained from evm tracing |
| 123 | + contracts_deployed_per_tx = BLOCK_GAS_LIMIT // approximate_gas_per_deployment |
| 124 | + |
| 125 | + deploy_txs = [] |
| 126 | + |
| 127 | + def generate_deploy_tx(contracts_to_deploy: int): |
| 128 | + return Transaction( |
| 129 | + to=factory_caller_address, |
| 130 | + gas_limit=BLOCK_GAS_LIMIT, |
| 131 | + gas_price=10**9, # Bump required due to the amount of full blocks |
| 132 | + data=Hash(contracts_deployed_per_tx), |
| 133 | + sender=pre.fund_eoa(), |
| 134 | + ) |
| 135 | + |
| 136 | + for _ in range(total_contracts_to_deploy // contracts_deployed_per_tx): |
| 137 | + deploy_txs.append(generate_deploy_tx(contracts_deployed_per_tx)) |
| 138 | + |
| 139 | + if total_contracts_to_deploy % contracts_deployed_per_tx != 0: |
| 140 | + deploy_txs.append( |
| 141 | + generate_deploy_tx(total_contracts_to_deploy % contracts_deployed_per_tx) |
| 142 | + ) |
| 143 | + |
| 144 | + post = {} |
| 145 | + deployed_contract_addresses = [] |
| 146 | + for i in range(total_contracts_to_deploy): |
| 147 | + deployed_contract_address = compute_create_address( |
| 148 | + address=factory_address, |
| 149 | + nonce=i + 1, |
| 150 | + ) |
| 151 | + post[deployed_contract_address] = Account(nonce=1) |
| 152 | + deployed_contract_addresses.append(deployed_contract_address) |
| 153 | + |
| 154 | + opcode_code = ( |
| 155 | + sum(Op.POP(opcode(address=address)) for address in deployed_contract_addresses) + Op.STOP |
| 156 | + ) |
| 157 | + if len(opcode_code) > MAX_CONTRACT_SIZE: |
| 158 | + # TODO: A workaround could be to split the opcode code into multiple contracts |
| 159 | + # and call them in sequence. |
| 160 | + raise ValueError( |
| 161 | + f"Code size {len(opcode_code)} exceeds maximum code size {MAX_CONTRACT_SIZE}" |
| 162 | + ) |
| 163 | + opcode_address = pre.deploy_contract(code=opcode_code) |
| 164 | + opcode_tx = Transaction( |
| 165 | + to=opcode_address, |
| 166 | + gas_limit=OPCODE_GAS_LIMIT, |
| 167 | + gas_price=10**9, # Bump required due to the amount of full blocks |
| 168 | + sender=pre.fund_eoa(), |
| 169 | + ) |
| 170 | + |
| 171 | + blockchain_test( |
| 172 | + genesis_environment=env, |
| 173 | + pre=pre, |
| 174 | + post=post, |
| 175 | + blocks=[ |
| 176 | + *[Block(txs=[deploy_tx]) for deploy_tx in deploy_txs], |
| 177 | + Block(txs=[opcode_tx]), |
| 178 | + ], |
| 179 | + ) |
0 commit comments