Skip to content

Commit 26d7e56

Browse files
feat: add general benchmark helper
1 parent 8172882 commit 26d7e56

File tree

1 file changed

+199
-0
lines changed

1 file changed

+199
-0
lines changed

tests/benchmark/compute/helpers.py

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

0 commit comments

Comments
 (0)