Skip to content

Commit 25f0012

Browse files
authored
zkevm: add ECRECOVER, SHA2-256, IDENTITY and RIPEMD precompiles (#1524)
* zkevm: add ecrecover attack Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * lints Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * fix gas limits Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * generic precompiles with only data parameter Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * remove zkevm marks Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * lints Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * lints Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * cleanups Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * variable rename Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * fix comment Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * lints 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>
1 parent 58a8c84 commit 25f0012

File tree

1 file changed

+142
-30
lines changed

1 file changed

+142
-30
lines changed

tests/zkevm/test_worst_compute.py

Lines changed: 142 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,37 @@
1010
import pytest
1111

1212
from ethereum_test_forks import Fork
13-
from ethereum_test_tools import Alloc, Block, BlockchainTestFiller, Environment, Transaction
13+
from ethereum_test_tools import (
14+
Address,
15+
Alloc,
16+
Block,
17+
BlockchainTestFiller,
18+
Bytecode,
19+
Environment,
20+
Transaction,
21+
)
1422
from ethereum_test_tools.vm.opcode import Opcodes as Op
1523

1624
REFERENCE_SPEC_GIT_PATH = "TODO"
1725
REFERENCE_SPEC_VERSION = "TODO"
1826

1927
MAX_CODE_SIZE = 24 * 1024
2028
KECCAK_RATE = 136
29+
ECRECOVER_GAS_COST = 3_000
2130

2231

2332
@pytest.mark.valid_from("Cancun")
24-
@pytest.mark.parametrize(
25-
"gas_limit",
26-
[
27-
36_000_000,
28-
],
29-
)
3033
def test_worst_keccak(
3134
blockchain_test: BlockchainTestFiller,
3235
pre: Alloc,
3336
fork: Fork,
34-
gas_limit: int,
3537
):
3638
"""Test running a block with as many KECCAK256 permutations as possible."""
37-
env = Environment(gas_limit=gas_limit)
39+
env = Environment()
3840

3941
# Intrinsic gas cost is paid once.
4042
intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator()
41-
available_gas = gas_limit - intrinsic_gas_calculator()
43+
available_gas = env.gas_limit - intrinsic_gas_calculator()
4244

4345
gsc = fork.gas_costs()
4446
mem_exp_gas_calculator = fork.memory_expansion_gas_calculator()
@@ -90,11 +92,8 @@ def test_worst_keccak(
9092

9193
tx = Transaction(
9294
to=code_address,
93-
gas_limit=gas_limit,
94-
gas_price=10,
95+
gas_limit=env.gas_limit,
9596
sender=pre.fund_eoa(),
96-
data=[],
97-
value=0,
9897
)
9998

10099
blockchain_test(
@@ -105,22 +104,94 @@ def test_worst_keccak(
105104
)
106105

107106

108-
@pytest.mark.zkevm
109107
@pytest.mark.valid_from("Cancun")
110108
@pytest.mark.parametrize(
111-
"gas_limit",
109+
"address,static_cost,per_word_dynamic_cost,bytes_per_unit_of_work",
112110
[
113-
Environment().gas_limit,
111+
pytest.param(0x02, 60, 12, 64, id="SHA2-256"),
112+
pytest.param(0x03, 600, 120, 64, id="RIPEMD-160"),
113+
pytest.param(0x04, 15, 3, 1, id="IDENTITY"),
114114
],
115115
)
116+
def test_worst_precompile_only_data_input(
117+
blockchain_test: BlockchainTestFiller,
118+
pre: Alloc,
119+
fork: Fork,
120+
address: Address,
121+
static_cost: int,
122+
per_word_dynamic_cost: int,
123+
bytes_per_unit_of_work: int,
124+
):
125+
"""Test running a block with as many precompile calls which have a single `data` input."""
126+
env = Environment()
127+
128+
# Intrinsic gas cost is paid once.
129+
intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator()
130+
available_gas = env.gas_limit - intrinsic_gas_calculator()
131+
132+
gsc = fork.gas_costs()
133+
mem_exp_gas_calculator = fork.memory_expansion_gas_calculator()
134+
135+
# Discover the optimal input size to maximize precompile work, not precompile calls.
136+
max_work = 0
137+
optimal_input_length = 0
138+
for input_length in range(1, 1_000_000, 32):
139+
parameters_gas = (
140+
gsc.G_BASE # PUSH0 = arg offset
141+
+ gsc.G_BASE # PUSH0 = arg size
142+
+ gsc.G_BASE # PUSH0 = arg size
143+
+ gsc.G_VERY_LOW # PUSH0 = arg offset
144+
+ gsc.G_VERY_LOW # PUSHN = address
145+
+ gsc.G_BASE # GAS
146+
)
147+
iteration_gas_cost = (
148+
parameters_gas
149+
+ +static_cost # Precompile static cost
150+
+ math.ceil(input_length / 32) * per_word_dynamic_cost # Precompile dynamic cost
151+
+ gsc.G_BASE # POP
152+
)
153+
# From the available gas, we substract the mem expansion costs considering we know the
154+
# current input size length.
155+
available_gas_after_expansion = max(
156+
0, available_gas - mem_exp_gas_calculator(new_bytes=input_length)
157+
)
158+
# Calculate how many calls we can do.
159+
num_calls = available_gas_after_expansion // iteration_gas_cost
160+
total_work = num_calls * math.ceil(input_length / bytes_per_unit_of_work)
161+
162+
# If we found an input size that is better (reg permutations/gas), then save it.
163+
if total_work > max_work:
164+
max_work = total_work
165+
optimal_input_length = input_length
166+
167+
calldata = Op.CODECOPY(0, 0, optimal_input_length)
168+
attack_block = Op.POP(Op.STATICCALL(Op.GAS, address, 0, optimal_input_length, 0, 0))
169+
code = code_loop_precompile_call(calldata, attack_block)
170+
171+
code_address = pre.deploy_contract(code=code)
172+
173+
tx = Transaction(
174+
to=code_address,
175+
gas_limit=env.gas_limit,
176+
sender=pre.fund_eoa(),
177+
)
178+
179+
blockchain_test(
180+
env=env,
181+
pre=pre,
182+
post={},
183+
blocks=[Block(txs=[tx])],
184+
)
185+
186+
187+
@pytest.mark.valid_from("Cancun")
116188
def test_worst_modexp(
117189
blockchain_test: BlockchainTestFiller,
118190
pre: Alloc,
119191
fork: Fork,
120-
gas_limit: int,
121192
):
122193
"""Test running a block with as many MODEXP calls as possible."""
123-
env = Environment(gas_limit=gas_limit)
194+
env = Environment()
124195

125196
base_mod_length = 32
126197
exp_length = 32
@@ -144,23 +215,48 @@ def test_worst_modexp(
144215
iter_complexity = exp.bit_length() - 1
145216
gas_cost = math.floor((mul_complexity * iter_complexity) / 3)
146217
attack_block = Op.POP(Op.STATICCALL(gas_cost, 0x5, 0, 32 * 6, 0, 0))
218+
code = code_loop_precompile_call(calldata, attack_block)
147219

148-
# The attack contract is: JUMPDEST + [attack_block]* + PUSH0 + JUMP
149-
jumpdest = Op.JUMPDEST
150-
jump_back = Op.JUMP(len(calldata))
151-
max_iters_loop = (MAX_CODE_SIZE - len(calldata) - len(jumpdest) - len(jump_back)) // len(
152-
attack_block
220+
code_address = pre.deploy_contract(code=code)
221+
222+
tx = Transaction(
223+
to=code_address,
224+
gas_limit=env.gas_limit,
225+
sender=pre.fund_eoa(),
153226
)
154-
code = calldata + jumpdest + sum([attack_block] * max_iters_loop) + jump_back
155-
if len(code) > MAX_CODE_SIZE:
156-
# Must never happen, but keep it as a sanity check.
157-
raise ValueError(f"Code size {len(code)} exceeds maximum code size {MAX_CODE_SIZE}")
158227

159-
code_address = pre.deploy_contract(code=code)
228+
blockchain_test(
229+
env=env,
230+
pre=pre,
231+
post={},
232+
blocks=[Block(txs=[tx])],
233+
)
234+
235+
236+
@pytest.mark.valid_from("Cancun")
237+
def test_worst_ecrecover(
238+
blockchain_test: BlockchainTestFiller,
239+
pre: Alloc,
240+
fork: Fork,
241+
):
242+
"""Test running a block with as many ECRECOVER calls as possible."""
243+
env = Environment()
244+
245+
# Calldata
246+
calldata = (
247+
Op.MSTORE(0 * 32, 0x38D18ACB67D25C8BB9942764B62F18E17054F66A817BD4295423ADF9ED98873E)
248+
+ Op.MSTORE(1 * 32, 27)
249+
+ Op.MSTORE(2 * 32, 0x38D18ACB67D25C8BB9942764B62F18E17054F66A817BD4295423ADF9ED98873E)
250+
+ Op.MSTORE(3 * 32, 0x789D1DD423D25F0772D2748D60F7E4B81BB14D086EBA8E8E8EFB6DCFF8A4AE02)
251+
)
252+
253+
attack_block = Op.POP(Op.STATICCALL(ECRECOVER_GAS_COST, 0x1, 0, 32 * 4, 0, 0))
254+
code = code_loop_precompile_call(calldata, attack_block)
255+
code_address = pre.deploy_contract(code=bytes(code))
160256

161257
tx = Transaction(
162258
to=code_address,
163-
gas_limit=gas_limit,
259+
gas_limit=env.gas_limit,
164260
sender=pre.fund_eoa(),
165261
)
166262

@@ -170,3 +266,19 @@ def test_worst_modexp(
170266
post={},
171267
blocks=[Block(txs=[tx])],
172268
)
269+
270+
271+
def code_loop_precompile_call(calldata: Bytecode, attack_block: Bytecode):
272+
"""Create a code loop that calls a precompile with the given calldata."""
273+
# The attack contract is: CALLDATA_PREP + #JUMPDEST + [attack_block]* + JUMP(#)
274+
jumpdest = Op.JUMPDEST
275+
jump_back = Op.JUMP(len(calldata))
276+
max_iters_loop = (MAX_CODE_SIZE - len(calldata) - len(jumpdest) - len(jump_back)) // len(
277+
attack_block
278+
)
279+
code = calldata + jumpdest + sum([attack_block] * max_iters_loop) + jump_back
280+
if len(code) > MAX_CODE_SIZE:
281+
# Must never happen, but keep it as a sanity check.
282+
raise ValueError(f"Code size {len(code)} exceeds maximum code size {MAX_CODE_SIZE}")
283+
284+
return code

0 commit comments

Comments
 (0)