Skip to content

chore(tests|forks): add max blobs per tx limit #1884

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Users can select any of the artifacts depending on their testing needs for their

- 🔀 Refactored `BLOBHASH` opcode context tests to use the `pre_alloc` plugin in order to avoid contract and EOA address collisions ([#1637](https://github.com/ethereum/execution-spec-tests/pull/1637)).
- 🔀 Refactored `SELFDESTRUCT` opcode collision tests to use the `pre_alloc` plugin in order to avoid contract and EOA address collisions ([#1643](https://github.com/ethereum/execution-spec-tests/pull/1643)).
- ✨ EIP-7594: Sanity test cases to send blob transactions and verify `engine_getBlobsVX` using the `execute` command ([#1644](https://github.com/ethereum/execution-spec-tests/pull/1644)).
- ✨ EIP-7594: Sanity test cases to send blob transactions and verify `engine_getBlobsVX` using the `execute` command ([#1644](https://github.com/ethereum/execution-spec-tests/pull/1644),[#1884](https://github.com/ethereum/execution-spec-tests/pull/1884)).
- 🔀 Refactored EIP-145 static tests into python ([#1683](https://github.com/ethereum/execution-spec-tests/pull/1683)).
- ✨ EIP-7823, EIP-7883: Add test cases for ModExp precompile gas-cost updates and input limits on Osaka ([#1579](https://github.com/ethereum/execution-spec-tests/pull/1579), [#1729](https://github.com/ethereum/execution-spec-tests/pull/1729)).
- ✨ [EIP-7825](https://eips.ethereum.org/EIPS/eip-7825): Add test cases for the transaction gas limit of 30M gas ([#1711](https://github.com/ethereum/execution-spec-tests/pull/1711)).
Expand All @@ -102,6 +102,7 @@ Users can select any of the artifacts depending on their testing needs for their
- ✨ [EIP-7934](https://eips.ethereum.org/EIPS/eip-7934): Add test cases for the block RLP max limit of 10MiB ([#1730](https://github.com/ethereum/execution-spec-tests/pull/1730)).
- ✨ [EIP-7939](https://eips.ethereum.org/EIPS/eip-7939) Add count leading zeros (CLZ) opcode tests for Osaka ([#1733](https://github.com/ethereum/execution-spec-tests/pull/1733)).
- ✨ [EIP-7918](https://eips.ethereum.org/EIPS/eip-7918): Blob base fee bounded by execution cost test cases (initial), includes some adjustments to [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) tests ([#1685](https://github.com/ethereum/execution-spec-tests/pull/1685)).
- 🔀 Adds the max blob transaction limit to the tests including updates to [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) for Osaka ([#1884](https://github.com/ethereum/execution-spec-tests/pull/1884)).

## [v4.5.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v4.5.0) - 2025-05-14

Expand Down
6 changes: 6 additions & 0 deletions src/ethereum_test_forks/base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,12 @@ def target_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> in
"""Return the target blobs per block at a given fork."""
pass

@classmethod
@abstractmethod
def max_blobs_per_tx(cls, block_number: int = 0, timestamp: int = 0) -> int:
"""Return the max blobs per transaction at a given fork."""
pass

@classmethod
@abstractmethod
def max_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int:
Expand Down
23 changes: 19 additions & 4 deletions src/ethereum_test_forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ def max_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int:
"""Return the max number of blobs per block at a given fork."""
raise NotImplementedError(f"Max blobs per block is not supported in {cls.name()}")

@classmethod
def max_blobs_per_tx(cls, block_number: int = 0, timestamp: int = 0) -> int:
"""Return the max number of blobs per tx at a given fork."""
raise NotImplementedError(f"Max blobs per tx is not supported in {cls.name()}")

@classmethod
def blob_schedule(cls, block_number: int = 0, timestamp: int = 0) -> BlobSchedule | None:
"""At genesis, no blob schedule is used."""
Expand Down Expand Up @@ -1006,14 +1011,19 @@ def supports_blobs(cls, block_number: int = 0, timestamp: int = 0) -> bool:

@classmethod
def target_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int:
"""Blobs are enabled starting from Cancun, with a static target of 3 blobs."""
"""Blobs are enabled starting from Cancun, with a static target of 3 blobs per block."""
return 3

@classmethod
def max_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int:
"""Blobs are enabled starting from Cancun, with a static max of 6 blobs."""
"""Blobs are enabled starting from Cancun, with a static max of 6 blobs per block."""
return 6

@classmethod
def max_blobs_per_tx(cls, block_number: int = 0, timestamp: int = 0) -> int:
"""Blobs are enabled starting from Cancun, with a static max equal to the max per block."""
return cls.max_blobs_per_block(block_number, timestamp)

@classmethod
def blob_schedule(cls, block_number: int = 0, timestamp: int = 0) -> BlobSchedule | None:
"""
Expand Down Expand Up @@ -1256,12 +1266,12 @@ def blob_base_fee_update_fraction(cls, block_number: int = 0, timestamp: int = 0

@classmethod
def target_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int:
"""Target blob count of 6 for Prague."""
"""Blobs in Prague, have a static target of 6 blobs per block."""
return 6

@classmethod
def max_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int:
"""Max blob count of 9 for Prague."""
"""Blobs in Prague, have a static max of 9 blobs per block."""
return 9

@classmethod
Expand Down Expand Up @@ -1462,6 +1472,11 @@ def fn(

return fn

@classmethod
def max_blobs_per_tx(cls, block_number: int = 0, timestamp: int = 0) -> int:
"""Blobs in Osaka, have a static max of 6 blobs per tx. Differs from the max per block."""
return 6


class EOFv1(Prague, solc_name="cancun"):
"""EOF fork."""
Expand Down
68 changes: 43 additions & 25 deletions tests/cancun/eip4844_blobs/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from ethereum_test_forks import Fork
from ethereum_test_forks import Fork, Osaka
from ethereum_test_tools import Alloc, Block, Environment, Hash, Transaction, add_kzg_version

from .spec import Spec
Expand All @@ -26,6 +26,12 @@ def max_blobs_per_block(fork: Fork) -> int:
return fork.max_blobs_per_block()


@pytest.fixture
def max_blobs_per_tx(fork: Fork) -> int:
"""Return max number of blobs per transaction."""
return fork.max_blobs_per_tx()


@pytest.fixture
def blob_gas_per_blob(fork: Fork) -> int:
"""Return default blob gas cost per blob."""
Expand Down Expand Up @@ -269,6 +275,10 @@ def non_zero_blob_gas_used_genesis_block(
genesis value, expecting an appropriate drop to the intermediate block.
Similarly, we must add parent_blobs to the intermediate block within
a blob tx such that an equivalent blobGasUsed field is wrote.

For forks >= Osaka where the MAX_BLOBS_PER_TX is introduced, we
split the blobs across multiple transactions to respect the
MAX_BLOBS_PER_TX limit.
"""
if parent_blobs == 0:
return None
Expand All @@ -287,30 +297,38 @@ def non_zero_blob_gas_used_genesis_block(
)

sender = pre.fund_eoa(10**27)

# Address that contains no code, nor balance and is not a contract.
empty_account_destination = pre.fund_eoa(0)

blob_gas_price_calculator = fork.blob_gas_price_calculator(block_number=1)

return Block(
txs=[
Transaction(
ty=Spec.BLOB_TX_TYPE,
sender=sender,
to=empty_account_destination,
value=1,
gas_limit=21_000,
max_fee_per_gas=tx_max_fee_per_gas,
max_priority_fee_per_gas=0,
max_fee_per_blob_gas=blob_gas_price_calculator(
excess_blob_gas=parent_excess_blob_gas
),
access_list=[],
blob_versioned_hashes=add_kzg_version(
[Hash(x) for x in range(parent_blobs)],
Spec.BLOB_COMMITMENT_VERSION_KZG,
),
)
]
)
# Split blobs into chunks for forks >= Osaka only to respect MAX_BLOBS_PER_TX limits.
# This allows us to keep the creation of single transactions for Cancun/Prague where the
# MAX_BLOBS_PER_TX is not enforced, hitting coverage for block level blob gas validation
# when parent_blobs > MAX_BLOBS_PER_BLOCK.
max_blobs_per_tx = fork.max_blobs_per_tx() if fork >= Osaka else parent_blobs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could check whether max per block != max per tx instead of checking explicitly for Osaka.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think I had this initially but changed it to this to see if it would fix the coverage! I will revert back.

blob_chunks = [
range(i, min(i + max_blobs_per_tx, parent_blobs))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.

for i in range(0, parent_blobs, max_blobs_per_tx)
]

def create_blob_transaction(blob_range):
return Transaction(
ty=Spec.BLOB_TX_TYPE,
sender=sender,
to=empty_account_destination,
value=1,
gas_limit=21_000,
max_fee_per_gas=tx_max_fee_per_gas,
max_priority_fee_per_gas=0,
max_fee_per_blob_gas=blob_gas_price_calculator(
excess_blob_gas=parent_excess_blob_gas,
),
access_list=[],
blob_versioned_hashes=add_kzg_version(
[Hash(x) for x in blob_range],
Spec.BLOB_COMMITMENT_VERSION_KZG,
),
)

txs = [create_blob_transaction(chunk) for chunk in blob_chunks]

return Block(txs=txs)
21 changes: 15 additions & 6 deletions tests/cancun/eip4844_blobs/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def get_min_excess_blobs_for_blob_gas_price(
def get_blob_combinations(
cls,
blob_count: int,
max_blobs_per_tx: int,
) -> List[Tuple[int, ...]]:
"""Get all possible combinations of blobs that result in a given blob count."""
combinations = [
Expand All @@ -128,10 +129,11 @@ def get_blob_combinations(
blob_count + 1, 0, -1
) # We can have from 1 to at most MAX_BLOBS_PER_BLOCK blobs per block
for seq in itertools.combinations_with_replacement(
range(1, blob_count + 2), i
range(1, min(blob_count + 1, max_blobs_per_tx) + 1), i
) # We iterate through all possible combinations
if sum(seq) == blob_count # And we only keep the ones that match the
# expected invalid blob count
if sum(seq)
== blob_count # And we only keep the ones that match the expected blob count
and all(tx_blobs <= max_blobs_per_tx for tx_blobs in seq) # Validate each tx
]

# We also add the reversed version of each combination, only if it's not
Expand All @@ -146,12 +148,14 @@ def get_blob_combinations(
def all_valid_blob_combinations(cls, fork: Fork) -> List[Tuple[int, ...]]:
"""
Return all valid blob tx combinations for a given block,
assuming the given MAX_BLOBS_PER_BLOCK.
assuming the given MAX_BLOBS_PER_BLOCK, whilst respecting MAX_BLOBS_PER_TX.
"""
max_blobs_per_block = fork.max_blobs_per_block()
max_blobs_per_tx = fork.max_blobs_per_tx()

combinations: List[Tuple[int, ...]] = []
for i in range(1, max_blobs_per_block + 1):
combinations += cls.get_blob_combinations(i)
combinations += cls.get_blob_combinations(i, max_blobs_per_tx)
return combinations

@classmethod
Expand All @@ -161,4 +165,9 @@ def invalid_blob_combinations(cls, fork: Fork) -> List[Tuple[int, ...]]:
MAX_BLOBS_PER_BLOCK+1 blobs.
"""
max_blobs_per_block = fork.max_blobs_per_block()
return cls.get_blob_combinations(max_blobs_per_block + 1)
max_blobs_per_tx = fork.max_blobs_per_tx()
invalid_combinations = cls.get_blob_combinations(
max_blobs_per_block + 1,
max_blobs_per_tx,
)
return invalid_combinations
8 changes: 4 additions & 4 deletions tests/cancun/eip4844_blobs/test_blob_txs.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ def test_insufficient_balance_blob_tx(
"blobs_per_tx",
lambda fork: [
pytest.param([1], id="single_blob"),
pytest.param([fork.max_blobs_per_block()], id="max_blobs"),
pytest.param([fork.max_blobs_per_tx()], id="max_blobs"),
],
)
@pytest.mark.parametrize(
Expand Down Expand Up @@ -699,7 +699,7 @@ def test_sufficient_balance_blob_tx(
"blobs_per_tx",
lambda fork: [
pytest.param([1], id="single_blob"),
pytest.param([fork.max_blobs_per_block()], id="max_blobs"),
pytest.param([fork.max_blobs_per_tx()], id="max_blobs"),
],
)
@pytest.mark.parametrize(
Expand Down Expand Up @@ -764,7 +764,7 @@ def test_sufficient_balance_blob_tx_pre_fund_tx(
"blobs_per_tx",
lambda fork: [
pytest.param([1], id="single_blob"),
pytest.param([fork.max_blobs_per_block()], id="max_blobs"),
pytest.param([fork.max_blobs_per_tx()], id="max_blobs"),
],
)
@pytest.mark.parametrize(
Expand Down Expand Up @@ -875,7 +875,7 @@ def generate_invalid_tx_blob_count_tests(
id="too_few_blobs",
),
pytest.param(
[fork.max_blobs_per_block() + 1],
[fork.max_blobs_per_tx() + 1],
[
TransactionException.TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED,
TransactionException.TYPE_3_TX_BLOB_COUNT_EXCEEDED,
Expand Down
7 changes: 2 additions & 5 deletions tests/cancun/eip4844_blobs/test_blob_txs_full.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,8 @@ def blocks(
def generate_full_blob_tests(
fork: Fork,
) -> List:
"""
Return a list of tests for invalid blob transactions due to insufficient max fee per blob gas
parametrized for each different fork.
"""
max_blobs = fork.max_blobs_per_block()
"""Return a list of test cases for full blob transactions."""
max_blobs = fork.max_blobs_per_tx()
return [
pytest.param(
[ # Txs
Expand Down
Loading
Loading