Skip to content

Commit a672724

Browse files
jsignmarioevz
authored andcommitted
feat(tests): zkevm: add bytecode worst case
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> lints Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> fix Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> ruff fix Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> simplify Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> fix linter Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> test: Implement dynamic contract deployment for worst bytecode zkevm test fix: review comments
1 parent 5cf9be9 commit a672724

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed

tests/zkevm/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""abstract: Tests for zkVMs."""

tests/zkevm/test_worst_bytecode.py

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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

Comments
 (0)