Skip to content

Commit 50ec823

Browse files
refactor: update logic and add benchmark tests
1 parent c489a91 commit 50ec823

File tree

3 files changed

+112
-8
lines changed

3 files changed

+112
-8
lines changed

src/ethereum_test_benchmark/benchmark_code_generator.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
"""Benchmark code generator classes for creating optimized bytecode patterns."""
22

3-
from dataclasses import dataclass
4-
53
from ethereum_test_forks import Fork
64
from ethereum_test_specs.benchmark import BenchmarkCodeGenerator
75
from ethereum_test_types import Alloc, Transaction
86
from ethereum_test_vm import Bytecode
97
from ethereum_test_vm.opcode import Opcodes as Op
108

119

12-
@dataclass
1310
class JumpLoopGenerator(BenchmarkCodeGenerator):
1411
"""Generates bytecode that loops execution using JUMP operations."""
1512

@@ -32,7 +29,6 @@ def generate_transaction(self, pre: Alloc, gas_limit: int, fork: Fork) -> Transa
3229
)
3330

3431

35-
@dataclass
3632
class ExtCallGenerator(BenchmarkCodeGenerator):
3733
"""Generates bytecode that fills the contract to maximum allowed code size."""
3834

@@ -43,11 +39,13 @@ def deploy_contracts(self, pre: Alloc, fork: Fork) -> None:
4339
# 1. The target contract that executes certain operation but not loop (e.g. PUSH)
4440
# 2. The loop contract that calls the target contract in a loop
4541

46-
max_stack_height = fork.max_stack_height()
42+
max_iterations = min(
43+
fork.max_stack_height(), fork.max_code_size() // len(self.attack_block)
44+
)
4745

4846
# Deploy target contract that contains the actual attack block
4947
self._target_contract_address = pre.deploy_contract(
50-
code=self.attack_block * max_stack_height
48+
code=self.attack_block * max_iterations
5149
)
5250

5351
# Create caller contract that repeatedly calls the target contract

src/ethereum_test_specs/benchmark.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Ethereum benchmark test spec definition and filler."""
22

3+
import math
34
from abc import ABC, abstractmethod
45
from dataclasses import dataclass, field
56
from typing import Callable, ClassVar, Dict, Generator, List, Sequence, Type
@@ -141,11 +142,11 @@ def split_transaction(self, tx: Transaction, gas_limit_cap: int | None) -> List[
141142
return [tx]
142143

143144
if gas_limit_cap >= self.gas_benchmark_value:
144-
tx.gas_limit = HexNumber(min(tx.gas_limit, self.gas_benchmark_value))
145+
tx.gas_limit = HexNumber(self.gas_benchmark_value)
145146
return [tx]
146147

148+
num_splits = math.ceil(self.gas_benchmark_value / gas_limit_cap)
147149
remaining_gas = self.gas_benchmark_value
148-
num_splits = remaining_gas // gas_limit_cap + int(remaining_gas % gas_limit_cap)
149150

150151
split_transactions = []
151152
for i in range(num_splits):
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Tests for the BenchmarkTest class and its transaction splitting functionality."""
2+
3+
import pytest
4+
5+
from ethereum_test_base_types import HexNumber
6+
from ethereum_test_specs.benchmark import BenchmarkTest
7+
from ethereum_test_types import Alloc, Environment, Transaction
8+
9+
10+
@pytest.mark.parametrize(
11+
"gas_benchmark_value_millions,expected_splits",
12+
[
13+
(1, 1), # 1M / 16M = 1 transaction
14+
(10, 1), # 10M / 16M = 1 transaction
15+
(30, 2), # 30M / 16M = 2 transactions (16M + 14M)
16+
(45, 3), # 45M / 16M = 3 transactions (16M + 16M + 13M)
17+
(60, 4), # 60M / 16M = 4 transactions (16M + 16M + 16M + 12M)
18+
(100, 7), # 100M / 16M = 7 transactions (6x16M + 4M)
19+
(150, 10), # 150M / 16M = 10 transactions (9x16M + 6M)
20+
],
21+
)
22+
def test_split_transaction(gas_benchmark_value_millions: int, expected_splits: int):
23+
"""Test that transaction splitting works correctly for Osaka fork gas cap."""
24+
gas_benchmark_value = gas_benchmark_value_millions * 1_000_000
25+
gas_limit_cap = 16_000_000 # Osaka's transaction gas limit cap
26+
27+
# Create a minimal BenchmarkTest instance
28+
benchmark_test = BenchmarkTest(
29+
pre=Alloc(),
30+
post=Alloc(),
31+
tx=Transaction(sender=HexNumber(0), to=HexNumber(0), nonce=0),
32+
env=Environment(),
33+
gas_benchmark_value=gas_benchmark_value,
34+
)
35+
36+
# Test the split_transaction method
37+
assert benchmark_test.tx is not None, "Transaction should not be None"
38+
split_txs = benchmark_test.split_transaction(benchmark_test.tx, gas_limit_cap)
39+
40+
# Verify the number of transactions
41+
assert len(split_txs) == expected_splits, (
42+
f"Expected {expected_splits} transactions for {gas_benchmark_value_millions}M gas, "
43+
f"got {len(split_txs)}"
44+
)
45+
46+
# Verify total gas equals the benchmark value
47+
total_gas = sum(tx.gas_limit for tx in split_txs)
48+
assert total_gas == gas_benchmark_value, (
49+
f"Total gas {total_gas} doesn't match benchmark value {gas_benchmark_value}"
50+
)
51+
52+
# Verify no transaction exceeds the cap
53+
for i, tx in enumerate(split_txs):
54+
assert tx.gas_limit <= gas_limit_cap, (
55+
f"Transaction {i} gas limit {tx.gas_limit} exceeds cap {gas_limit_cap}"
56+
)
57+
58+
# Verify nonces increment correctly
59+
for i, tx in enumerate(split_txs):
60+
assert tx.nonce == i, f"Transaction {i} has incorrect nonce {tx.nonce}"
61+
62+
# Verify gas distribution
63+
for i, tx in enumerate(split_txs[:-1]): # All but last should be at cap
64+
assert tx.gas_limit == gas_limit_cap, (
65+
f"Transaction {i} should have gas limit {gas_limit_cap}, got {tx.gas_limit}"
66+
)
67+
68+
# Last transaction should have the remainder
69+
if expected_splits > 1:
70+
expected_last_gas = gas_benchmark_value - (gas_limit_cap * (expected_splits - 1))
71+
assert split_txs[-1].gas_limit == expected_last_gas, (
72+
f"Last transaction should have {expected_last_gas} gas, got {split_txs[-1].gas_limit}"
73+
)
74+
75+
76+
@pytest.mark.parametrize(
77+
"gas_benchmark_value,gas_limit_cap",
78+
[
79+
(50_000_000, None), # No cap - should return single transaction
80+
(50_000_000, 100_000_000), # Cap higher than benchmark value
81+
],
82+
)
83+
def test_split_transaction_edge_cases(gas_benchmark_value: int, gas_limit_cap: int | None):
84+
"""Test edge cases for transaction splitting."""
85+
benchmark_test = BenchmarkTest(
86+
pre=Alloc(),
87+
post=Alloc(),
88+
tx=Transaction(sender=HexNumber(0), to=HexNumber(0), nonce=0, gas_limit=1_000_000_000),
89+
env=Environment(),
90+
gas_benchmark_value=gas_benchmark_value,
91+
)
92+
93+
assert benchmark_test.tx is not None, "Transaction should not be None"
94+
split_txs = benchmark_test.split_transaction(benchmark_test.tx, gas_limit_cap)
95+
96+
# Should return single transaction in both cases
97+
assert len(split_txs) == 1, f"Expected 1 transaction, got {len(split_txs)}"
98+
99+
if gas_limit_cap is None:
100+
# When no cap, gas_limit should be benchmark value
101+
assert split_txs[0].gas_limit == gas_benchmark_value
102+
else:
103+
# When cap > benchmark, gas_limit should be min of tx.gas_limit and benchmark
104+
assert benchmark_test.tx is not None, "Transaction should not be None"
105+
assert split_txs[0].gas_limit == min(benchmark_test.tx.gas_limit, gas_benchmark_value)

0 commit comments

Comments
 (0)