9
9
10
10
import pytest
11
11
12
+ from ethereum_test_base_types import Address
12
13
from ethereum_test_benchmark .benchmark_code_generator import JumpLoopGenerator
13
14
from ethereum_test_forks import Fork
14
15
from ethereum_test_tools import (
35
36
XOR_TABLE = [Hash (i ).sha256 () for i in range (XOR_TABLE_SIZE )]
36
37
37
38
38
- @pytest .mark .parametrize (
39
- "opcode" ,
40
- [
41
- Op .EXTCODESIZE ,
42
- Op .EXTCODEHASH ,
43
- Op .CALL ,
44
- Op .CALLCODE ,
45
- Op .DELEGATECALL ,
46
- Op .STATICCALL ,
47
- Op .EXTCODECOPY ,
48
- ],
49
- )
50
- def test_worst_bytecode_single_opcode (
51
- blockchain_test : BlockchainTestFiller ,
52
- pre : Alloc ,
53
- fork : Fork ,
54
- opcode : Op ,
55
- env : Environment ,
56
- gas_benchmark_value : int ,
57
- ):
58
- """
59
- Test a block execution where a single opcode execution maxes out the gas limit,
60
- and the opcodes access a huge amount of contract code.
61
-
62
- We first use a single block to deploy a factory contract that will be used to deploy
63
- a large number of contracts.
64
-
65
- This is done to avoid having a big pre-allocation size for the test.
66
-
67
- The test is performed in the last block of the test, and the entire block gas limit is
68
- consumed by repeated opcode executions.
69
- """
70
- # The attack gas limit is the gas limit which the target tx will use
71
- # The test will scale the block gas limit to setup the contracts accordingly to be
72
- # able to pay for the contract deposit. This has to take into account the 200 gas per byte,
73
- # but also the quadratic memory expansion costs which have to be paid each time the
74
- # memory is being setup
75
- attack_gas_limit = gas_benchmark_value
76
- max_contract_size = fork .max_code_size ()
77
-
78
- gas_costs = fork .gas_costs ()
79
-
80
- # Calculate the absolute minimum gas costs to deploy the contract
81
- # This does not take into account setting up the actual memory (using KECCAK256 and XOR)
82
- # so the actual costs of deploying the contract is higher
83
- memory_expansion_gas_calculator = fork .memory_expansion_gas_calculator ()
84
- memory_gas_minimum = memory_expansion_gas_calculator (new_bytes = len (bytes (max_contract_size )))
85
- code_deposit_gas_minimum = (
86
- fork .gas_costs ().G_CODE_DEPOSIT_BYTE * max_contract_size + memory_gas_minimum
87
- )
88
-
89
- intrinsic_gas_cost_calc = fork .transaction_intrinsic_cost_calculator ()
90
- # Calculate the loop cost of the attacker to query one address
91
- loop_cost = (
92
- gas_costs .G_KECCAK_256 # KECCAK static cost
93
- + math .ceil (85 / 32 ) * gas_costs .G_KECCAK_256_WORD # KECCAK dynamic cost for CREATE2
94
- + gas_costs .G_VERY_LOW * 3 # ~MSTOREs+ADDs
95
- + gas_costs .G_COLD_ACCOUNT_ACCESS # Opcode cost
96
- + 30 # ~Gluing opcodes
97
- )
98
- # Calculate the number of contracts to be targeted
99
- num_contracts = (
100
- # Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs)
101
- attack_gas_limit - intrinsic_gas_cost_calc () - gas_costs .G_VERY_LOW * 4
102
- ) // loop_cost
103
-
104
- # Set the block gas limit to a relative high value to ensure the code deposit tx
105
- # fits in the block (there is enough gas available in the block to execute this)
106
- minimum_gas_limit = code_deposit_gas_minimum * 2 * num_contracts
107
- if env .gas_limit < minimum_gas_limit :
108
- raise Exception (
109
- f"`BENCHMARKING_MAX_GAS` ({ env .gas_limit } ) is no longer enough to support this test, "
110
- f"which requires { minimum_gas_limit } gas for its setup. Update the value or consider "
111
- "optimizing gas usage during the setup phase of this test."
112
- )
113
-
39
+ def deploy_initcode_template (pre : Alloc , fork : Fork ) -> tuple [Address , Bytecode ]:
40
+ """Deploy the initcode template contract."""
114
41
# The initcode will take its address as a starting point to the input to the keccak
115
42
# hash function.
116
43
# It will reuse the output of the hash function in a loop to create a large amount of
@@ -127,16 +54,19 @@ def test_worst_bytecode_single_opcode(
127
54
)
128
55
+ Op .POP
129
56
),
130
- condition = Op .LT (Op .MSIZE , max_contract_size ),
57
+ condition = Op .LT (Op .MSIZE , fork . max_code_size () ),
131
58
)
132
59
# Despite the whole contract has random bytecode, we make the first opcode be a STOP
133
60
# so CALL-like attacks return as soon as possible, while EXTCODE(HASH|SIZE) work as
134
61
# intended.
135
62
+ Op .MSTORE8 (0 , 0x00 )
136
- + Op .RETURN (0 , max_contract_size )
63
+ + Op .RETURN (0 , fork . max_code_size () )
137
64
)
138
- initcode_address = pre .deploy_contract (code = initcode )
65
+ return pre .deploy_contract (code = initcode ), initcode
139
66
67
+
68
+ def deploy_factory_contract (pre : Alloc , fork : Fork , initcode_address : Address ) -> Address :
69
+ """Deploy the factory contract."""
140
70
# The factory contract will simply use the initcode that is already deployed,
141
71
# and create a new contract and return its address if successful.
142
72
factory_code = (
@@ -158,75 +88,200 @@ def test_worst_bytecode_single_opcode(
158
88
+ Op .SSTORE (0 , Op .ADD (Op .SLOAD (0 ), 1 ))
159
89
+ Op .RETURN (0 , 32 )
160
90
)
161
- factory_address = pre .deploy_contract (code = factory_code )
91
+ return pre .deploy_contract (code = factory_code )
92
+
162
93
94
+ def deploy_factory_caller_contract (pre : Alloc , fork : Fork , factory_address : Address ) -> Address :
95
+ """Deploy the factory caller contract."""
163
96
# The factory caller will call the factory contract N times, creating N new contracts.
164
97
# Calldata should contain the N value.
165
98
factory_caller_code = Op .CALLDATALOAD (0 ) + While (
166
99
body = Op .POP (Op .CALL (address = factory_address )),
167
100
condition = Op .PUSH1 (1 ) + Op .SWAP1 + Op .SUB + Op .DUP1 + Op .ISZERO + Op .ISZERO ,
168
101
)
169
- factory_caller_address = pre .deploy_contract (code = factory_caller_code )
170
102
171
- contracts_deployment_tx = Transaction (
172
- to = factory_caller_address ,
173
- gas_limit = env .gas_limit ,
174
- gas_price = 10 ** 6 ,
175
- data = Hash (num_contracts ),
176
- sender = pre .fund_eoa (),
103
+ return pre .deploy_contract (code = factory_caller_code )
104
+
105
+
106
+ def deploy_attack_contract (
107
+ pre : Alloc , fork : Fork , factory_address : Address , initcode : Bytecode , opcode : Op
108
+ ) -> Address :
109
+ """Deploy the attack contract."""
110
+ # Setup memory for later CREATE2 address generation loop.
111
+ # 0xFF+[Address(20bytes)]+[seed(32bytes)]+[initcode keccak(32bytes)]
112
+ setup = (
113
+ Op .MSTORE (0 , factory_address )
114
+ + Op .MSTORE8 (32 - 20 - 1 , 0xFF )
115
+ + Op .CALLDATACOPY (dest_offset = 32 , offset = 0 , size = 32 )
116
+ + Op .MSTORE (64 , initcode .keccak256 ())
177
117
)
178
118
179
- post = {}
180
- deployed_contract_addresses = []
181
- for i in range (num_contracts ):
182
- deployed_contract_address = compute_create2_address (
183
- address = factory_address ,
184
- salt = i ,
185
- initcode = initcode ,
186
- )
187
- post [deployed_contract_address ] = Account (nonce = 1 )
188
- deployed_contract_addresses .append (deployed_contract_address )
119
+ # setup_cost: G_VERY_LOW * 9 (PUSH) + G_VERY_LOW * 3 (MSTORE) + G_VERY_LOW (CALLDATACOPY)
189
120
121
+ # Attack call
190
122
attack_call = Bytecode ()
191
123
if opcode == Op .EXTCODECOPY :
192
124
attack_call = Op .EXTCODECOPY (address = Op .SHA3 (32 - 20 - 1 , 85 ), dest_offset = 96 , size = 1000 )
193
125
else :
194
126
# For the rest of the opcodes, we can use the same generic attack call
195
127
# since all only minimally need the `address` of the target.
196
128
attack_call = Op .POP (opcode (address = Op .SHA3 (32 - 20 - 1 , 85 )))
197
- attack_code = (
198
- # Setup memory for later CREATE2 address generation loop.
199
- # 0xFF+[Address(20bytes)]+[seed(32bytes)]+[initcode keccak(32bytes)]
200
- Op .MSTORE (0 , factory_address )
201
- + Op .MSTORE8 (32 - 20 - 1 , 0xFF )
202
- + Op .MSTORE (32 , 0 )
203
- + Op .MSTORE (64 , initcode .keccak256 ())
204
- # Main loop
205
- + While (
206
- body = attack_call + Op .MSTORE (32 , Op .ADD (Op .MLOAD (32 ), 1 )),
207
- )
129
+
130
+ attack_code = setup + While (
131
+ body = attack_call + Op .MSTORE (32 , Op .ADD (Op .MLOAD (32 ), 1 )),
208
132
)
209
133
210
- if len (attack_code ) > max_contract_size :
211
- # TODO: A workaround could be to split the opcode code into multiple contracts
212
- # and call them in sequence.
213
- raise ValueError (
214
- f"Code size { len (attack_code )} exceeds maximum code size { max_contract_size } "
215
- )
216
- opcode_address = pre .deploy_contract (code = attack_code )
217
- opcode_tx = Transaction (
218
- to = opcode_address ,
219
- gas_limit = attack_gas_limit ,
220
- gas_price = 10 ** 9 ,
221
- sender = pre .fund_eoa (),
134
+ # loop_cost = (
135
+ # gas_costs.G_KECCAK_256 KECCAK static cost
136
+ # + math.ceil(85 / 32) * gas_costs.G_KECCAK_256_WORD KECCAK dynamic cost for CREATE2
137
+ # + gas_costs.G_VERY_LOW * ~MSTOREs+ADDs
138
+ # + gas_costs.G_COLD_ACCOUNT_ACCESS Opcode cost
139
+ # + 30 ~Gluing opcodes
140
+ # )
141
+
142
+ return pre .deploy_contract (code = attack_code )
143
+
144
+
145
+ @pytest .mark .parametrize (
146
+ "opcode" ,
147
+ [
148
+ Op .EXTCODESIZE ,
149
+ Op .EXTCODEHASH ,
150
+ Op .CALL ,
151
+ Op .CALLCODE ,
152
+ Op .DELEGATECALL ,
153
+ Op .STATICCALL ,
154
+ Op .EXTCODECOPY ,
155
+ ],
156
+ )
157
+ def test_worst_bytecode_single_opcode (
158
+ blockchain_test : BlockchainTestFiller ,
159
+ pre : Alloc ,
160
+ fork : Fork ,
161
+ opcode : Op ,
162
+ env : Environment ,
163
+ gas_benchmark_value : int ,
164
+ tx_gas_limit_cap : int ,
165
+ ):
166
+ """
167
+ Test a block execution where a single opcode execution maxes out the gas limit,
168
+ and the opcodes access a huge amount of contract code.
169
+
170
+ We first use a single block to deploy a factory contract that will be used to deploy
171
+ a large number of contracts.
172
+
173
+ This is done to avoid having a big pre-allocation size for the test.
174
+
175
+ The test is performed in the last block of the test, and the entire block gas limit is
176
+ consumed by repeated opcode executions.
177
+ """
178
+ iteration_count = gas_benchmark_value // tx_gas_limit_cap
179
+
180
+ # The attack gas limit is the gas limit which the target tx will use
181
+ # The test will scale the block gas limit to setup the contracts accordingly to be
182
+ # able to pay for the contract deposit. This has to take into account the 200 gas per byte,
183
+ # but also the quadratic memory expansion costs which have to be paid each time the
184
+ # memory is being setup
185
+ max_contract_size = fork .max_code_size ()
186
+
187
+ gas_costs = fork .gas_costs ()
188
+
189
+ intrinsic_gas_cost_calc = fork .transaction_intrinsic_cost_calculator ()
190
+ setup_cost = gas_costs .G_VERY_LOW * 13
191
+ # Calculate the loop cost of the attacker to query one address
192
+ loop_cost = (
193
+ gas_costs .G_KECCAK_256 # KECCAK static cost
194
+ + math .ceil (85 / 32 ) * gas_costs .G_KECCAK_256_WORD # KECCAK dynamic cost for CREATE2
195
+ + gas_costs .G_VERY_LOW * 3 # ~MSTOREs+ADDs
196
+ + gas_costs .G_COLD_ACCOUNT_ACCESS # Opcode cost
197
+ + 30 # ~Gluing opcodes
222
198
)
223
199
200
+ total_contracts = 0
201
+ gas_remaining = gas_benchmark_value
202
+ for _ in range (iteration_count ):
203
+ gas_available = min (tx_gas_limit_cap , gas_remaining )
204
+ total_contracts += (
205
+ # Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs)
206
+ gas_available - intrinsic_gas_cost_calc () - setup_cost
207
+ ) // loop_cost
208
+ gas_remaining -= gas_available
209
+
210
+ # Deployment Phase - Deploy factory contract
211
+ initcode_address , initcode = deploy_initcode_template (pre , fork )
212
+ factory_address = deploy_factory_contract (pre , fork , initcode_address )
213
+ factory_caller_address = deploy_factory_caller_contract (pre , fork , factory_address )
214
+
215
+ # Deployment Phase - Deploy N contracts
216
+
217
+ # Calculate the absolute minimum gas costs to deploy the contract
218
+ # This does not take into account setting up the actual memory (using KECCAK256 and XOR)
219
+ # so the actual costs of deploying the contract is higher
220
+ memory_expansion_gas_calculator = fork .memory_expansion_gas_calculator ()
221
+ memory_gas_minimum = memory_expansion_gas_calculator (new_bytes = len (bytes (max_contract_size )))
222
+ code_deposit_gas_minimum = (
223
+ fork .gas_costs ().G_CODE_DEPOSIT_BYTE * max_contract_size + memory_gas_minimum
224
+ )
225
+
226
+ contracts_deployment_txs = []
227
+ deployment_cost_per_iteration = code_deposit_gas_minimum * 3
228
+ deployed_contract_num = 0
229
+ gas_remaining = gas_benchmark_value
230
+
231
+ while deployed_contract_num < total_contracts :
232
+ gas_available = min (tx_gas_limit_cap , gas_remaining )
233
+ gas_remaining -= gas_available
234
+ num = (gas_available - intrinsic_gas_cost_calc ()) // deployment_cost_per_iteration
235
+ deployed_contract_num += num
236
+ contracts_deployment_txs .append (
237
+ Transaction (
238
+ to = factory_caller_address ,
239
+ gas_limit = gas_available ,
240
+ data = Hash (num ),
241
+ sender = pre .fund_eoa (),
242
+ )
243
+ )
244
+
245
+ # Attack Phase
246
+ opcode_address = deploy_attack_contract (pre , fork , factory_address , initcode , opcode )
247
+
248
+ opcode_txs = []
249
+ gas_remaining = gas_benchmark_value
250
+ access_contract_index = 0
251
+
252
+ for _ in range (iteration_count ):
253
+ gas_available = min (tx_gas_limit_cap , gas_remaining )
254
+ gas_remaining -= gas_available
255
+ num = (gas_available - intrinsic_gas_cost_calc () - setup_cost ) // loop_cost
256
+ opcode_txs .append (
257
+ Transaction (
258
+ to = opcode_address ,
259
+ data = Hash (access_contract_index ),
260
+ gas_limit = gas_available ,
261
+ sender = pre .fund_eoa (),
262
+ )
263
+ )
264
+ access_contract_index += num
265
+
266
+ # Post State Verification
267
+ post = {}
268
+ deployed_contract_addresses = []
269
+ for i in range (total_contracts ):
270
+ deployed_contract_address = compute_create2_address (
271
+ address = factory_address ,
272
+ salt = i ,
273
+ initcode = initcode ,
274
+ )
275
+ post [deployed_contract_address ] = Account (nonce = 1 )
276
+ deployed_contract_addresses .append (deployed_contract_address )
277
+
278
+ # Blockchain Test Execution
224
279
blockchain_test (
225
280
pre = pre ,
226
281
post = post ,
227
282
blocks = [
228
- Block (txs = [ contracts_deployment_tx ] ),
229
- Block (txs = [ opcode_tx ] ),
283
+ Block (txs = contracts_deployment_txs ),
284
+ Block (txs = opcode_txs ),
230
285
],
231
286
exclude_full_post_state_in_output = True ,
232
287
)
0 commit comments