Skip to content

Commit dfd9334

Browse files
committed
test: Implement dynamic contract deployment for worst bytecode zkevm test
1 parent 7a2392f commit dfd9334

File tree

1 file changed

+134
-26
lines changed

1 file changed

+134
-26
lines changed

tests/zkevm/test_worst_bytecode.py

Lines changed: 134 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,59 +8,167 @@
88
import pytest
99

1010
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+
)
1222
from ethereum_test_tools.vm.opcode import Opcodes as Op
1323

1424
REFERENCE_SPEC_GIT_PATH = "TODO"
1525
REFERENCE_SPEC_VERSION = "TODO"
1626

1727
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)]
2032

2133

34+
# TODO: Parametrize for EOF
2235
@pytest.mark.zkevm
23-
@pytest.mark.valid_from("Cancun")
2436
@pytest.mark.parametrize(
25-
"num_called_contracts",
37+
"opcode",
2638
[
27-
1,
28-
# 10,
29-
# MAX_NUM_CONTRACT_CALLS
39+
Op.EXTCODESIZE,
40+
Op.EXTCODEHASH,
3041
],
3142
)
32-
def test_worst_bytecode(
43+
@pytest.mark.valid_from("Cancun")
44+
def test_worst_bytecode_single_opcode(
3345
blockchain_test: BlockchainTestFiller,
3446
pre: Alloc,
3547
fork: Fork,
36-
num_called_contracts: int,
48+
opcode: Op,
3749
):
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+
"""
3962
env = Environment(gas_limit=GAS_LIMIT)
4063

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),
4581
)
82+
+ Op.RETURN(0, MAX_CONTRACT_SIZE)
83+
)
84+
initcode_address = pre.deploy_contract(code=initcode)
4685

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)
49104
)
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)
51156

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,
54161
gas_limit=GAS_LIMIT,
55-
gas_price=10,
162+
gas_price=10**9, # Bump required due to the amount of full blocks
56163
sender=pre.fund_eoa(),
57-
data=[],
58-
value=0,
59164
)
60165

61166
blockchain_test(
62-
env=env,
167+
genesis_environment=env,
63168
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+
],
66174
)

0 commit comments

Comments
 (0)