Skip to content

Commit e4e958c

Browse files
jsignmarioevz
andauthored
feat(tests/zkevm): add stateful opcodes attacks (#1611)
* fix(github/coverage): Use skip-evm-dump for fill * fix(tox): Add `block-gas-limit` to zkevm tox * fix(github/coverage): Add `--block-gas-limit 36000000` to coverage * zkevm: add cold BALANCE Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkvm: add absent case for BALANCE Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkvm: add warm BALANCE tests Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkevm: include warm test for extcodesize and extcodehash Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkvm: add cold EXTCODEHASH Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * rename parameter Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkvm: warm CALL-like opcodes Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkvm: cold SSTORE/SSLOAD Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkvm: add cold SSTORE/SLOAD Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkvm: add warm SSTORE/SLOAD Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * name parameters Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkvm: add blockhash test Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkvm: add SELFBALANCE test Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkevm: add warm EXTCODECOPY tests Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * simplify parameters Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * simplify parameters Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * add CALL/CALLCODE/DELEGATECALL/STATICCALL to worst bytecode attack Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkvm: add bytecode worst case for CALL-like opcodes Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkvm: add cold EXTCODECOPY Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * lints Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * fixes Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * fixes and lints Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * mark slow Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * improvement Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * add comment Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * rename parameter Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * bump gas limit Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * return gas limit to original value Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * review feedback Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> --------- Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> Co-authored-by: Mario Vega <marioevz@gmail.com>
1 parent 4a0fbf5 commit e4e958c

File tree

2 files changed

+520
-41
lines changed

2 files changed

+520
-41
lines changed

tests/zkevm/test_worst_bytecode.py

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
Alloc,
1616
Block,
1717
BlockchainTestFiller,
18+
Bytecode,
1819
Environment,
1920
Hash,
2021
Transaction,
@@ -36,12 +37,12 @@
3637
"opcode",
3738
[
3839
Op.EXTCODESIZE,
39-
],
40-
)
41-
@pytest.mark.parametrize(
42-
"attack_gas_limit",
43-
[
44-
Environment().gas_limit,
40+
Op.EXTCODEHASH,
41+
Op.CALL,
42+
Op.CALLCODE,
43+
Op.DELEGATECALL,
44+
Op.STATICCALL,
45+
Op.EXTCODECOPY,
4546
],
4647
)
4748
@pytest.mark.slow()
@@ -51,7 +52,6 @@ def test_worst_bytecode_single_opcode(
5152
pre: Alloc,
5253
fork: Fork,
5354
opcode: Op,
54-
attack_gas_limit: int,
5555
):
5656
"""
5757
Test a block execution where a single opcode execution maxes out the gas limit,
@@ -68,6 +68,7 @@ def test_worst_bytecode_single_opcode(
6868
# We use 100G gas limit to be able to deploy a large number of contracts in a single block,
6969
# avoiding bloating the number of preparing blocks in the test.
7070
env = Environment(gas_limit=100_000_000_000)
71+
attack_gas_limit = Environment().gas_limit
7172

7273
# The initcode will take its address as a starting point to the input to the keccak
7374
# hash function.
@@ -87,6 +88,10 @@ def test_worst_bytecode_single_opcode(
8788
),
8889
condition=Op.LT(Op.MSIZE, MAX_CONTRACT_SIZE),
8990
)
91+
# Despite the whole contract has random bytecode, we make the first opcode be a STOP
92+
# so CALL-like attacks return as soon as possible, while EXTCODE(HASH|SIZE) work as
93+
# intended.
94+
+ Op.MSTORE8(0, 0x00)
9095
+ Op.RETURN(0, MAX_CONTRACT_SIZE)
9196
)
9297
initcode_address = pre.deploy_contract(code=initcode)
@@ -105,7 +110,7 @@ def test_worst_bytecode_single_opcode(
105110
Op.CREATE2(
106111
value=0,
107112
offset=0,
108-
size=Op.MSIZE,
113+
size=Op.EXTCODESIZE(initcode_address),
109114
salt=Op.SLOAD(0),
110115
),
111116
)
@@ -128,40 +133,25 @@ def test_worst_bytecode_single_opcode(
128133
gas_costs.G_KECCAK_256 # KECCAK static cost
129134
+ math.ceil(85 / 32) * gas_costs.G_KECCAK_256_WORD # KECCAK dynamic cost for CREATE2
130135
+ gas_costs.G_VERY_LOW * 3 # ~MSTOREs+ADDs
131-
+ gas_costs.G_COLD_ACCOUNT_ACCESS # EXTCODESIZE
136+
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Opcode cost
132137
+ 30 # ~Gluing opcodes
133138
)
134-
max_number_of_contract_calls = (
139+
num_contracts = (
135140
# Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs)
136141
attack_gas_limit - intrinsic_gas_cost_calc() - gas_costs.G_VERY_LOW * 4
137142
) // loop_cost
138143

139-
total_contracts_to_deploy = max_number_of_contract_calls
140-
approximate_gas_per_deployment = 4_970_000 # Obtained from evm tracing
141-
contracts_deployed_per_tx = env.gas_limit // approximate_gas_per_deployment
142-
143-
deploy_txs = []
144-
145-
def generate_deploy_tx(contracts_to_deploy: int):
146-
return Transaction(
147-
to=factory_caller_address,
148-
gas_limit=env.gas_limit,
149-
gas_price=10**9, # Bump required due to the amount of full blocks
150-
data=Hash(contracts_deployed_per_tx),
151-
sender=pre.fund_eoa(),
152-
)
153-
154-
for _ in range(total_contracts_to_deploy // contracts_deployed_per_tx):
155-
deploy_txs.append(generate_deploy_tx(contracts_deployed_per_tx))
156-
157-
if total_contracts_to_deploy % contracts_deployed_per_tx != 0:
158-
deploy_txs.append(
159-
generate_deploy_tx(total_contracts_to_deploy % contracts_deployed_per_tx)
160-
)
144+
contracts_deployment_tx = Transaction(
145+
to=factory_caller_address,
146+
gas_limit=env.gas_limit,
147+
gas_price=10**9,
148+
data=Hash(num_contracts),
149+
sender=pre.fund_eoa(),
150+
)
161151

162152
post = {}
163153
deployed_contract_addresses = []
164-
for i in range(total_contracts_to_deploy):
154+
for i in range(num_contracts):
165155
deployed_contract_address = compute_create2_address(
166156
address=factory_address,
167157
salt=i,
@@ -170,29 +160,37 @@ def generate_deploy_tx(contracts_to_deploy: int):
170160
post[deployed_contract_address] = Account(nonce=1)
171161
deployed_contract_addresses.append(deployed_contract_address)
172162

173-
opcode_code = (
163+
attack_call = Bytecode()
164+
if opcode == Op.EXTCODECOPY:
165+
attack_call = Op.EXTCODECOPY(address=Op.SHA3(32 - 20 - 1, 85), dest_offset=85, size=1000)
166+
else:
167+
# For the rest of the opcodes, we can use the same generic attack call
168+
# since all only minimally need the `address` of the target.
169+
attack_call = Op.POP(opcode(address=Op.SHA3(32 - 20 - 1, 85)))
170+
attack_code = (
174171
# Setup memory for later CREATE2 address generation loop.
172+
# 0xFF+[Address(20bytes)]+[seed(32bytes)]+[initcode keccak(32bytes)]
175173
Op.MSTORE(0, factory_address)
176-
+ Op.MSTORE8(32 - 20 - 1, 0xFF) # 0xFF prefix byte
174+
+ Op.MSTORE8(32 - 20 - 1, 0xFF)
177175
+ Op.MSTORE(32, 0)
178176
+ Op.MSTORE(64, initcode.keccak256())
179177
# Main loop
180178
+ While(
181-
body=Op.POP(opcode(Op.SHA3(32 - 20 - 1, 85))) + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)),
179+
body=attack_call + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)),
182180
)
183181
)
184182

185-
if len(opcode_code) > MAX_CONTRACT_SIZE:
183+
if len(attack_code) > MAX_CONTRACT_SIZE:
186184
# TODO: A workaround could be to split the opcode code into multiple contracts
187185
# and call them in sequence.
188186
raise ValueError(
189-
f"Code size {len(opcode_code)} exceeds maximum code size {MAX_CONTRACT_SIZE}"
187+
f"Code size {len(attack_code)} exceeds maximum code size {MAX_CONTRACT_SIZE}"
190188
)
191-
opcode_address = pre.deploy_contract(code=opcode_code)
189+
opcode_address = pre.deploy_contract(code=attack_code)
192190
opcode_tx = Transaction(
193191
to=opcode_address,
194192
gas_limit=attack_gas_limit,
195-
gas_price=10**9, # Bump required due to the amount of full blocks
193+
gas_price=10**9,
196194
sender=pre.fund_eoa(),
197195
)
198196

@@ -201,7 +199,7 @@ def generate_deploy_tx(contracts_to_deploy: int):
201199
pre=pre,
202200
post=post,
203201
blocks=[
204-
*[Block(txs=[deploy_tx]) for deploy_tx in deploy_txs],
202+
Block(txs=[contracts_deployment_tx]),
205203
Block(txs=[opcode_tx]),
206204
],
207205
exclude_full_post_state_in_output=True,

0 commit comments

Comments
 (0)