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