1010import pytest
1111
1212from 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+ )
1422from ethereum_test_tools .vm .opcode import Opcodes as Op
1523
1624REFERENCE_SPEC_GIT_PATH = "TODO"
1725REFERENCE_SPEC_VERSION = "TODO"
1826
1927MAX_CODE_SIZE = 24 * 1024
2028KECCAK_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- )
3033def 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" )
116188def 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