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