Skip to content

Commit 533299f

Browse files
refactor(tests): General benchmark folder restructuring (#1681)
1 parent 2857040 commit 533299f

31 files changed

+4332
-3502
lines changed

tests/benchmark/compute/helpers.py

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
"""Helper functions for the EVM benchmark worst-case tests."""
2+
3+
import math
4+
from enum import Enum, auto
5+
from typing import Sequence, cast
6+
7+
from execution_testing import Fork, Hash, Op
8+
9+
from tests.osaka.eip7951_p256verify_precompiles.spec import (
10+
BytesConcatenation as P256BytesConcatenation,
11+
)
12+
from tests.osaka.eip7951_p256verify_precompiles.spec import (
13+
FieldElement,
14+
)
15+
from tests.prague.eip2537_bls_12_381_precompiles.spec import (
16+
BytesConcatenation as BLSBytesConcatenation,
17+
)
18+
19+
DEFAULT_BINOP_ARGS = (
20+
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,
21+
0x73EDA753299D7D483339D80809A1D80553BDA402FFFE5BFEFFFFFFFF00000001,
22+
)
23+
24+
XOR_TABLE_SIZE = 256
25+
XOR_TABLE = [Hash(i).sha256() for i in range(XOR_TABLE_SIZE)]
26+
27+
28+
class StorageAction:
29+
"""Enum for storage actions."""
30+
31+
READ = auto()
32+
WRITE_SAME_VALUE = auto()
33+
WRITE_NEW_VALUE = auto()
34+
35+
36+
class TransactionResult:
37+
"""Enum for the possible transaction outcomes."""
38+
39+
SUCCESS = auto()
40+
OUT_OF_GAS = auto()
41+
REVERT = auto()
42+
43+
44+
class ReturnDataStyle(Enum):
45+
"""Helper enum to specify return data is returned to the caller."""
46+
47+
RETURN = auto()
48+
REVERT = auto()
49+
IDENTITY = auto()
50+
51+
52+
class CallDataOrigin:
53+
"""Enum for calldata origins."""
54+
55+
TRANSACTION = auto()
56+
CALL = auto()
57+
58+
59+
def neg(x: int) -> int:
60+
"""Negate the given integer in the two's complement 256-bit range."""
61+
assert 0 <= x < 2**256
62+
return 2**256 - x
63+
64+
65+
def make_dup(index: int) -> Op:
66+
"""
67+
Create a DUP instruction which duplicates the index-th (counting from 0)
68+
element from the top of the stack. E.g. make_dup(0) → DUP1.
69+
"""
70+
assert 0 <= index < 16, f"DUP index {index} out of range [0, 15]"
71+
return getattr(Op, f"DUP{index + 1}")
72+
73+
74+
def to_signed(x: int) -> int:
75+
"""Convert an unsigned integer to a signed integer."""
76+
return x if x < 2**255 else x - 2**256
77+
78+
79+
def to_unsigned(x: int) -> int:
80+
"""Convert a signed integer to an unsigned integer."""
81+
return x if x >= 0 else x + 2**256
82+
83+
84+
def shr(x: int, s: int) -> int:
85+
"""Shift right."""
86+
return x >> s
87+
88+
89+
def shl(x: int, s: int) -> int:
90+
"""Shift left."""
91+
return x << s
92+
93+
94+
def sar(x: int, s: int) -> int:
95+
"""Arithmetic shift right."""
96+
return to_unsigned(to_signed(x) >> s)
97+
98+
99+
def concatenate_parameters(
100+
parameters: (
101+
Sequence[str]
102+
| Sequence[P256BytesConcatenation]
103+
| Sequence[BLSBytesConcatenation]
104+
| Sequence[bytes]
105+
),
106+
) -> bytes:
107+
"""
108+
Concatenate precompile parameters into bytes.
109+
110+
Args:
111+
parameters: List of parameters, either as hex strings or byte objects
112+
(bytes, BytesConcatenation, or FieldElement).
113+
114+
Returns:
115+
Concatenated bytes from all parameters.
116+
117+
"""
118+
if all(isinstance(p, str) for p in parameters):
119+
parameters_str = cast(Sequence[str], parameters)
120+
concatenated_hex_string = "".join(parameters_str)
121+
return bytes.fromhex(concatenated_hex_string)
122+
elif all(
123+
isinstance(
124+
p,
125+
(
126+
bytes,
127+
P256BytesConcatenation,
128+
BLSBytesConcatenation,
129+
FieldElement,
130+
),
131+
)
132+
for p in parameters
133+
):
134+
parameters_bytes_list = [
135+
bytes(p)
136+
for p in cast(
137+
Sequence[
138+
P256BytesConcatenation
139+
| BLSBytesConcatenation
140+
| bytes
141+
| FieldElement
142+
],
143+
parameters,
144+
)
145+
]
146+
return b"".join(parameters_bytes_list)
147+
else:
148+
raise TypeError(
149+
"parameters must be a sequence of strings (hex) "
150+
"or a sequence of byte-like objects (bytes, BytesConcatenation or "
151+
"FieldElement)."
152+
)
153+
154+
155+
def calculate_optimal_input_length(
156+
available_gas: int,
157+
fork: Fork,
158+
static_cost: int,
159+
per_word_dynamic_cost: int,
160+
bytes_per_unit_of_work: int,
161+
) -> int:
162+
"""
163+
Calculate the optimal input length to maximize precompile work.
164+
165+
This function finds the input size that maximizes the total amount of
166+
work (in terms of bytes processed) a precompile can perform given a
167+
fixed gas budget. It balances the trade-off between making more calls
168+
with smaller inputs versus fewer calls with larger inputs.
169+
170+
Args:
171+
available_gas: Total gas available for precompile calls.
172+
fork: The fork to use for gas cost calculations.
173+
static_cost: Static gas cost per precompile call.
174+
per_word_dynamic_cost: Dynamic gas cost per 32-byte word of input.
175+
bytes_per_unit_of_work: Number of bytes processed per unit of work.
176+
177+
Returns:
178+
The optimal input length in bytes that maximizes total work.
179+
180+
"""
181+
gsc = fork.gas_costs()
182+
mem_exp_gas_calculator = fork.memory_expansion_gas_calculator()
183+
184+
max_work = 0
185+
optimal_input_length = 0
186+
187+
for input_length in range(1, 1_000_000, 32):
188+
parameters_gas = (
189+
gsc.G_BASE # PUSH0 = arg offset
190+
+ gsc.G_BASE # PUSH0 = arg size
191+
+ gsc.G_BASE # PUSH0 = arg size
192+
+ gsc.G_VERY_LOW # PUSH0 = arg offset
193+
+ gsc.G_VERY_LOW # PUSHN = address
194+
+ gsc.G_BASE # GAS
195+
)
196+
iteration_gas_cost = (
197+
parameters_gas
198+
+ static_cost # Precompile static cost
199+
+ math.ceil(input_length / 32) * per_word_dynamic_cost
200+
# Precompile dynamic cost
201+
+ gsc.G_BASE # POP
202+
)
203+
204+
# From the available gas, subtract the memory expansion costs
205+
# considering the current input size length.
206+
available_gas_after_expansion = max(
207+
0, available_gas - mem_exp_gas_calculator(new_bytes=input_length)
208+
)
209+
210+
# Calculate how many calls we can do.
211+
num_calls = available_gas_after_expansion // iteration_gas_cost
212+
total_work = num_calls * math.ceil(
213+
input_length / bytes_per_unit_of_work
214+
)
215+
216+
# If we found an input size with better total work, save it.
217+
if total_work > max_work:
218+
max_work = total_work
219+
optimal_input_length = input_length
220+
221+
return optimal_input_length

0 commit comments

Comments
 (0)