Skip to content

Commit 72addb2

Browse files
chore(tests): use representative blob combos in high blob count forks (#2127)
* chore(tests): use representative blob combinations for EIP-4844 high-blob forks * fix: tox * fix: test IDs --------- Co-authored-by: Mario Vega <marioevz@gmail.com>
1 parent 72af492 commit 72addb2

1 file changed

Lines changed: 169 additions & 12 deletions

File tree

tests/cancun/eip4844_blobs/spec.py

Lines changed: 169 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Defines EIP-4844 specification constants and functions."""
22

33
import itertools
4+
import math
45
from dataclasses import dataclass
56
from hashlib import sha256
6-
from typing import List, Optional, Tuple
7+
from typing import List, Optional, Set, Tuple
78

8-
from execution_testing import Fork, Transaction
9+
import pytest
10+
from execution_testing import Fork, ParameterSet, Transaction
911

1012

1113
@dataclass(frozen=True)
@@ -87,6 +89,136 @@ class SpecHelpers:
8789
"""
8890

8991
BYTES_PER_FIELD_ELEMENT = 32
92+
_EXHAUSTIVE_MAX_BLOBS_PER_BLOCK = (
93+
9 # Osaka max; exhaustive is tractable up to here
94+
)
95+
96+
@classmethod
97+
def get_representative_blob_combinations(
98+
cls,
99+
blob_count: int,
100+
max_blobs_per_tx: int,
101+
) -> List[Tuple[int, ...]]:
102+
"""
103+
Get a bounded set of representative blob-per-tx partitions for a given
104+
blob count, instead of exhaustively enumerating all valid partitions.
105+
"""
106+
n = blob_count
107+
if n < 1:
108+
return []
109+
m = max_blobs_per_tx
110+
seen: Set[Tuple[int, ...]] = set()
111+
result: List[Tuple[int, ...]] = []
112+
113+
def add(combo: Tuple[int, ...]) -> None:
114+
if combo not in seen:
115+
seen.add(combo)
116+
result.append(combo)
117+
118+
# 1. Single tx (if it fits)
119+
# e.g. n=5, m=6 → (5,)
120+
if n <= m:
121+
add((n,))
122+
123+
# 2. All singles
124+
# e.g. n=10 → (1,1,1,1,1,1,1,1,1,1)
125+
if n > 1:
126+
add((1,) * n)
127+
128+
# 3. Greedy pack: fill max-sized txs first
129+
# e.g. n=10, m=6 → (6,4)
130+
if n > m:
131+
q, r = divmod(n, m)
132+
greedy = (m,) * q + ((r,) if r else ())
133+
add(greedy)
134+
135+
# 4. Reversed greedy
136+
# e.g. n=10, m=6 → (4,6)
137+
rev = tuple(reversed(greedy))
138+
add(rev)
139+
140+
# 5. One big tx + singles for the rest (and reversed)
141+
# e.g. n=10, m=6 → (6,1,1,1,1) and (1,1,1,1,6)
142+
if n > 1:
143+
big = min(n - 1, m)
144+
rest = n - big
145+
combo = (big,) + (1,) * rest
146+
add(combo)
147+
add(tuple(reversed(combo)))
148+
149+
# 6. Balanced split into two txs (and reversed)
150+
# e.g. n=10, m=6 → (5,5); n=9, m=6 → (5,4) and (4,5)
151+
if n > 1:
152+
half_hi = math.ceil(n / 2)
153+
half_lo = n - half_hi
154+
if half_hi <= m and half_lo >= 1:
155+
add((half_hi, half_lo))
156+
if half_hi != half_lo:
157+
add((half_lo, half_hi))
158+
159+
# 7. Uniform non-max: all txs same size, 1 < k < m
160+
# e.g. n=12, m=6 → (4,4,4); n=15, m=6 → (5,5,5)
161+
if n > 1:
162+
for k in range(m - 1, 1, -1):
163+
if n % k == 0 and n // k > 1:
164+
add((k,) * (n // k))
165+
break
166+
167+
return result
168+
169+
@classmethod
170+
def get_representative_invalid_blob_combinations(
171+
cls,
172+
fork: Fork,
173+
) -> List[Tuple[int, ...]]:
174+
"""
175+
Get a bounded set of representative invalid blob-per-tx partitions
176+
that exceed the block blob limit by exactly one.
177+
"""
178+
max_blobs_per_block = fork.max_blobs_per_block()
179+
max_blobs_per_tx = fork.max_blobs_per_tx()
180+
total = max_blobs_per_block + 1
181+
m = max_blobs_per_tx
182+
seen: Set[Tuple[int, ...]] = set()
183+
result: List[Tuple[int, ...]] = []
184+
185+
def add(combo: Tuple[int, ...]) -> None:
186+
if combo not in seen:
187+
seen.add(combo)
188+
result.append(combo)
189+
190+
# 1. Single oversized tx — e.g. (16,)
191+
add((total,))
192+
193+
# 2. Greedy pack of total — e.g. total=16, m=6 → (6,6,4)
194+
q, r = divmod(total, m)
195+
greedy = (m,) * q + ((r,) if r else ())
196+
add(greedy)
197+
198+
# 3. All singles — e.g. (1,)*16
199+
add((1,) * total)
200+
201+
# 4. One full tx + overflow — e.g. total=16, m=6 → (6,10)
202+
overflow = total - m
203+
if overflow >= 1:
204+
add((m, overflow))
205+
206+
# 5. One blob + full block — e.g. (1,21)
207+
# Per-tx-oversized elements must be last: the test sends all txs from
208+
# one sender with sequential nonces, so a rejected non-last tx creates
209+
# a nonce gap that causes subsequent txs to fail with NONCE_MISMATCH,
210+
# not the expected blob error.
211+
add((1, max_blobs_per_block))
212+
213+
# 6. Balanced all-valid: near-equal tx sizes, all within per-tx limit
214+
# e.g. total=16, m=6 → (6,5,5)
215+
num_txs = math.ceil(total / m)
216+
base, extra = divmod(total, num_txs)
217+
balanced = (base + 1,) * extra + (base,) * (num_txs - extra)
218+
if all(b <= m for b in balanced):
219+
add(balanced)
220+
221+
return result
90222

91223
@classmethod
92224
def get_min_excess_blob_gas_for_blob_gas_price(
@@ -166,30 +298,55 @@ def get_blob_combinations(
166298
return combinations
167299

168300
@classmethod
169-
def all_valid_blob_combinations(cls, fork: Fork) -> List[Tuple[int, ...]]:
301+
def all_valid_blob_combinations(cls, fork: Fork) -> List[ParameterSet]:
170302
"""
171303
Return all valid blob tx combinations for a given block, assuming the
172304
given MAX_BLOBS_PER_BLOCK, whilst respecting MAX_BLOBS_PER_TX.
173305
"""
174306
max_blobs_per_block = fork.max_blobs_per_block()
175307
max_blobs_per_tx = fork.max_blobs_per_tx()
308+
exhaustive = max_blobs_per_block <= cls._EXHAUSTIVE_MAX_BLOBS_PER_BLOCK
176309

177310
combinations: List[Tuple[int, ...]] = []
178311
for i in range(1, max_blobs_per_block + 1):
179-
combinations += cls.get_blob_combinations(i, max_blobs_per_tx)
180-
return combinations
312+
if exhaustive:
313+
combinations += cls.get_blob_combinations(i, max_blobs_per_tx)
314+
else:
315+
combinations += cls.get_representative_blob_combinations(
316+
i, max_blobs_per_tx
317+
)
318+
return [
319+
pytest.param(
320+
combination,
321+
id=f"blobs_per_tx_{repr(combination).replace(' ', '')}",
322+
)
323+
for combination in combinations
324+
]
181325

182326
@classmethod
183-
def invalid_blob_combinations(cls, fork: Fork) -> List[Tuple[int, ...]]:
327+
def invalid_blob_combinations(cls, fork: Fork) -> List[ParameterSet]:
184328
"""
185329
Return invalid blob tx combinations for a given block that use up to
186330
MAX_BLOBS_PER_BLOCK+1 blobs.
187331
"""
188332
max_blobs_per_block = fork.max_blobs_per_block()
189333
max_blobs_per_tx = fork.max_blobs_per_tx()
190-
invalid_combinations = cls.get_blob_combinations(
191-
max_blobs_per_block + 1,
192-
max_blobs_per_tx,
193-
)
194-
invalid_combinations.append((max_blobs_per_block + 1,))
195-
return invalid_combinations
334+
335+
invalid_combinations: List[Tuple[int, ...]] = []
336+
if max_blobs_per_block <= cls._EXHAUSTIVE_MAX_BLOBS_PER_BLOCK:
337+
invalid_combinations += cls.get_blob_combinations(
338+
max_blobs_per_block + 1,
339+
max_blobs_per_tx,
340+
)
341+
invalid_combinations.append((max_blobs_per_block + 1,))
342+
else:
343+
invalid_combinations = (
344+
cls.get_representative_invalid_blob_combinations(fork)
345+
)
346+
return [
347+
pytest.param(
348+
combination,
349+
id=f"blobs_per_tx_{repr(combination).replace(' ', '')}",
350+
)
351+
for combination in invalid_combinations
352+
]

0 commit comments

Comments
 (0)