2
2
Tests that benchmark EVMs in worst-case block scenarios.
3
3
"""
4
4
5
+ import math
5
6
import random
6
7
from typing import Generator , Tuple
7
8
14
15
Address ,
15
16
Alloc ,
16
17
AuthorizationTuple ,
18
+ BenchmarkTestFiller ,
17
19
Block ,
18
20
BlockchainTestFiller ,
19
21
Environment ,
20
22
Hash ,
21
- StateTestFiller ,
22
23
Transaction ,
23
24
)
24
25
from ethereum_test_vm import Opcodes as Op
@@ -114,7 +115,7 @@ def ether_transfer_case(
114
115
["a_to_a" , "a_to_b" , "diff_acc_to_b" , "a_to_diff_acc" , "diff_acc_to_diff_acc" ],
115
116
)
116
117
def test_block_full_of_ether_transfers (
117
- blockchain_test : BlockchainTestFiller ,
118
+ benchmark_test : BenchmarkTestFiller ,
118
119
pre : Alloc ,
119
120
env : Environment ,
120
121
case_id : str ,
@@ -158,8 +159,7 @@ def test_block_full_of_ether_transfers(
158
159
else {receiver : Account (balance = balance ) for receiver , balance in balances .items ()}
159
160
)
160
161
161
- blockchain_test (
162
- genesis_environment = env ,
162
+ benchmark_test (
163
163
pre = pre ,
164
164
post = post_state ,
165
165
blocks = [Block (txs = txs )],
@@ -179,18 +179,14 @@ def total_cost_standard_per_token() -> int:
179
179
return 4
180
180
181
181
182
- @pytest .mark .parametrize ("zero_byte" , [True , False ])
183
- def test_block_full_data (
184
- state_test : StateTestFiller ,
185
- pre : Alloc ,
186
- zero_byte : bool ,
187
- intrinsic_cost : int ,
182
+ def calldata_generator (
183
+ gas_amount : int ,
184
+ zero_byte : int ,
188
185
total_cost_floor_per_token : int ,
189
- gas_benchmark_value : int ,
190
- ) -> None :
191
- """Test a block with empty payload."""
192
- # Gas cost calculation based on EIP-7683:
193
- # (https://eips.ethereum.org/EIPS/eip-7683)
186
+ total_cost_standard_per_token : int ,
187
+ ):
188
+ """Calculate the calldata based on the gas amount and zero byte."""
189
+ # Gas cost calculation based on EIP-7683: (https://eips.ethereum.org/EIPS/eip-7683)
194
190
#
195
191
# tx.gasUsed = 21000 + max(
196
192
# STANDARD_TOKEN_COST * tokens_in_calldata
@@ -213,125 +209,161 @@ def test_block_full_data(
213
209
# Token accounting:
214
210
# tokens_in_calldata = zero_bytes + 4 * non_zero_bytes
215
211
#
216
- # So we calculate how many bytes we can fit into calldata based on
217
- # available gas.
218
-
219
- gas_available = gas_benchmark_value - intrinsic_cost
220
-
221
- # Calculate the token_in_calldata
222
- max_tokens_in_calldata = gas_available // total_cost_floor_per_token
223
- # Calculate the number of bytes that can be stored in the calldata
212
+ # So we calculate how many bytes we can fit into calldata based on available gas.
213
+ max_tokens_in_calldata = gas_amount // total_cost_floor_per_token
224
214
num_of_bytes = max_tokens_in_calldata if zero_byte else max_tokens_in_calldata // 4
225
215
byte_data = b"\x00 " if zero_byte else b"\xff "
216
+ return byte_data * num_of_bytes
226
217
227
- tx = Transaction (
228
- to = pre .fund_eoa (),
229
- data = byte_data * num_of_bytes ,
230
- gas_limit = gas_benchmark_value ,
231
- sender = pre .fund_eoa (),
232
- )
233
218
234
- state_test (
219
+ @pytest .mark .parametrize ("zero_byte" , [True , False ])
220
+ def test_block_full_data (
221
+ benchmark_test : BenchmarkTestFiller ,
222
+ pre : Alloc ,
223
+ zero_byte : bool ,
224
+ intrinsic_cost : int ,
225
+ total_cost_floor_per_token : int ,
226
+ gas_benchmark_value : int ,
227
+ tx_gas_limit_cap : int ,
228
+ total_cost_standard_per_token : int ,
229
+ fork : Fork ,
230
+ ):
231
+ """Test a block with empty payload."""
232
+ iteration_count = math .ceil (gas_benchmark_value / tx_gas_limit_cap )
233
+
234
+ gas_remaining = gas_benchmark_value
235
+ total_gas_used = 0
236
+ txs = []
237
+ for _ in range (iteration_count ):
238
+ gas_available = min (tx_gas_limit_cap , gas_remaining ) - intrinsic_cost
239
+ data = calldata_generator (
240
+ gas_available ,
241
+ zero_byte ,
242
+ total_cost_floor_per_token ,
243
+ total_cost_standard_per_token ,
244
+ )
245
+
246
+ total_gas_used += fork .transaction_intrinsic_cost_calculator ()(calldata = data )
247
+ gas_remaining -= gas_available + intrinsic_cost
248
+
249
+ txs .append (
250
+ Transaction (
251
+ to = pre .fund_eoa (),
252
+ data = data ,
253
+ gas_limit = gas_available + intrinsic_cost ,
254
+ sender = pre .fund_eoa (),
255
+ )
256
+ )
257
+
258
+ benchmark_test (
235
259
pre = pre ,
236
260
post = {},
237
- tx = tx ,
261
+ blocks = [Block (txs = txs )],
262
+ expected_benchmark_gas_used = total_gas_used ,
238
263
)
239
264
240
265
241
266
def test_block_full_access_list_and_data (
242
- state_test : StateTestFiller ,
267
+ benchmark_test : BenchmarkTestFiller ,
243
268
pre : Alloc ,
244
269
intrinsic_cost : int ,
245
270
total_cost_standard_per_token : int ,
246
271
fork : Fork ,
247
272
gas_benchmark_value : int ,
248
- ) -> None :
249
- """
250
- Test a block with access lists (60% gas) and calldata (40% gas) using
251
- random mixed bytes.
252
- """
253
- attack_gas_limit = gas_benchmark_value
254
- gas_available = attack_gas_limit - intrinsic_cost
273
+ tx_gas_limit_cap : int ,
274
+ ):
275
+ """Test a block with access lists (60% gas) and calldata (40% gas) using random mixed bytes."""
276
+ iteration_count = math .ceil (gas_benchmark_value / tx_gas_limit_cap )
255
277
256
- # Split available gas: 60% for access lists, 40% for calldata
257
- gas_for_access_list = int (gas_available * 0.6 )
258
- gas_for_calldata = int (gas_available * 0.4 )
278
+ gas_remaining = gas_benchmark_value
279
+ total_gas_used = 0
259
280
260
- # Access list gas costs from fork's gas_costs
261
- gas_costs = fork .gas_costs ()
262
- gas_per_address = gas_costs .G_ACCESS_LIST_ADDRESS
263
- gas_per_storage_key = gas_costs .G_ACCESS_LIST_STORAGE
264
-
265
- # Calculate number of storage keys we can fit
266
- gas_after_address = gas_for_access_list - gas_per_address
267
- num_storage_keys = gas_after_address // gas_per_storage_key
268
-
269
- # Create access list with 1 address and many storage keys
270
- access_address = Address ("0x1234567890123456789012345678901234567890" )
271
- storage_keys = []
272
- for i in range (num_storage_keys ):
273
- # Generate random-looking storage keys
274
- storage_keys .append (Hash (i ))
275
-
276
- access_list = [
277
- AccessList (
278
- address = access_address ,
279
- storage_keys = storage_keys ,
280
- )
281
- ]
281
+ txs = []
282
+ for _ in range (iteration_count ):
283
+ gas_available = min (tx_gas_limit_cap , gas_remaining ) - intrinsic_cost
284
+
285
+ # Split available gas: 60% for access lists, 40% for calldata
286
+ gas_for_access_list = int (gas_available * 0.6 )
287
+ gas_for_calldata = int (gas_available * 0.4 )
288
+
289
+ # Access list gas costs from fork's gas_costs
290
+ gas_costs = fork .gas_costs ()
291
+ gas_per_address = gas_costs .G_ACCESS_LIST_ADDRESS
292
+ gas_per_storage_key = gas_costs .G_ACCESS_LIST_STORAGE
293
+
294
+ # Calculate number of storage keys we can fit
295
+ gas_after_address = gas_for_access_list - gas_per_address
296
+ num_storage_keys = gas_after_address // gas_per_storage_key
297
+
298
+ # Create access list with 1 address and many storage keys
299
+ access_address = Address ("0x1234567890123456789012345678901234567890" )
300
+ storage_keys = []
301
+ for i in range (num_storage_keys ):
302
+ # Generate random-looking storage keys
303
+ storage_keys .append (Hash (i ))
304
+
305
+ access_list = [
306
+ AccessList (
307
+ address = access_address ,
308
+ storage_keys = storage_keys ,
309
+ )
310
+ ]
282
311
283
- # Calculate calldata with 29% of gas for zero bytes and 71% for non-zero
284
- # bytes
285
- # Token accounting: tokens_in_calldata = zero_bytes + 4 * non_zero_bytes
286
- # We want to split the gas budget:
287
- # - 29% of gas_for_calldata for zero bytes
288
- # - 71% of gas_for_calldata for non-zero bytes
312
+ # Calculate calldata with 29% of gas for zero bytes and 71% for non-zero bytes
313
+ # Token accounting: tokens_in_calldata = zero_bytes + 4 * non_zero_bytes
314
+ # We want to split the gas budget:
315
+ # - 29% of gas_for_calldata for zero bytes
316
+ # - 71% of gas_for_calldata for non-zero bytes
289
317
290
- max_tokens_in_calldata = gas_for_calldata // total_cost_standard_per_token
318
+ max_tokens_in_calldata = gas_for_calldata // total_cost_standard_per_token
291
319
292
- # Calculate how many tokens to allocate to each type
293
- tokens_for_zero_bytes = int (max_tokens_in_calldata * 0.29 )
294
- tokens_for_non_zero_bytes = max_tokens_in_calldata - tokens_for_zero_bytes
320
+ # Calculate how many tokens to allocate to each type
321
+ tokens_for_zero_bytes = int (max_tokens_in_calldata * 0.29 )
322
+ tokens_for_non_zero_bytes = max_tokens_in_calldata - tokens_for_zero_bytes
295
323
296
- # Convert tokens to actual byte counts
297
- # Zero bytes: 1 token per byte
298
- # Non-zero bytes: 4 tokens per byte
299
- num_zero_bytes = tokens_for_zero_bytes # 1 token = 1 zero byte
300
- # 4 tokens = 1 non-zero byte
301
- num_non_zero_bytes = tokens_for_non_zero_bytes // 4
324
+ # Convert tokens to actual byte counts
325
+ # Zero bytes: 1 token per byte
326
+ # Non-zero bytes: 4 tokens per byte
327
+ num_zero_bytes = tokens_for_zero_bytes # 1 token = 1 zero byte
328
+ num_non_zero_bytes = tokens_for_non_zero_bytes // 4 # 4 tokens = 1 non-zero byte
302
329
303
- # Create calldata with mixed bytes
304
- calldata = bytearray ()
330
+ # Create calldata with mixed bytes
331
+ calldata = bytearray ()
305
332
306
- # Add zero bytes
307
- calldata .extend (b"\x00 " * num_zero_bytes )
333
+ # Add zero bytes
334
+ calldata .extend (b"\x00 " * num_zero_bytes )
308
335
309
- # Add non-zero bytes (random values from 0x01 to 0xff)
310
- rng = random .Random (42 ) # For reproducibility
311
- for _ in range (num_non_zero_bytes ):
312
- calldata .append (rng .randint (1 , 255 ))
336
+ # Add non-zero bytes (random values from 0x01 to 0xff)
337
+ rng = random .Random (42 ) # For reproducibility
338
+ for _ in range (num_non_zero_bytes ):
339
+ calldata .append (rng .randint (1 , 255 ))
313
340
314
- # Shuffle the bytes to mix zero and non-zero bytes
315
- calldata_list = list (calldata )
316
- rng .shuffle (calldata_list )
317
- shuffled_calldata = bytes (calldata_list )
341
+ # Shuffle the bytes to mix zero and non-zero bytes
342
+ calldata_list = list (calldata )
343
+ rng .shuffle (calldata_list )
344
+ shuffled_calldata = bytes (calldata_list )
318
345
319
- tx = Transaction (
320
- to = pre .fund_eoa (amount = 0 ),
321
- data = shuffled_calldata ,
322
- gas_limit = attack_gas_limit ,
323
- sender = pre .fund_eoa (),
324
- access_list = access_list ,
325
- )
346
+ txs .append (
347
+ Transaction (
348
+ to = pre .fund_eoa (amount = 0 ),
349
+ data = shuffled_calldata ,
350
+ gas_limit = gas_available + intrinsic_cost ,
351
+ sender = pre .fund_eoa (),
352
+ access_list = access_list ,
353
+ )
354
+ )
326
355
327
- state_test (
328
- pre = pre ,
329
- post = {},
330
- tx = tx ,
331
- expected_benchmark_gas_used = fork .transaction_intrinsic_cost_calculator ()(
356
+ gas_remaining -= gas_for_access_list + intrinsic_cost
357
+ total_gas_used += fork .transaction_intrinsic_cost_calculator ()(
332
358
calldata = shuffled_calldata ,
333
359
access_list = access_list ,
334
- ),
360
+ )
361
+
362
+ benchmark_test (
363
+ pre = pre ,
364
+ post = {},
365
+ blocks = [Block (txs = txs )],
366
+ expected_benchmark_gas_used = total_gas_used ,
335
367
)
336
368
337
369
0 commit comments