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