Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
# Multiplier applied to a one-shot live fee-market query to absorb the gap
# between query timing and tx submission (basefee can climb a few blocks
# between the two; the bump keeps txs landing without per-tx requeries).
FEE_BUMP_MULTIPLIER = 1.5
FEE_BUMP_MULTIPLIER = 3.0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The max_fee_per_gas and other gas configuration values adjust based on network status. However, the FEE_MULTIPLIER is only 1.5x. For benchmarks with many blocks, the cumulative gas limit can exceed the value calculated at the start of the test, leading to lower base fee issues.



def pytest_addoption(parser: pytest.Parser) -> None:
Expand Down
9 changes: 7 additions & 2 deletions packages/testing/src/execution_testing/rpc/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1119,7 +1119,7 @@ def get_alloc(
)

all_calls: List[RPCCall] = []
# (address, per-account call_info list, call count)
# (address, per-account call_info list)
address_info: List[tuple[Address, List[tuple[str, Any]]]] = []

for address, account in alloc.root.items():
Expand All @@ -1129,7 +1129,12 @@ def get_alloc(
all_calls.extend(calls)
address_info.append((address, call_info))

responses = self.post_batch_request(calls=all_calls)
responses: List[JSONRPCResponse] = []
batch_size = self.max_transactions_per_batch
for i in range(0, len(all_calls), batch_size):
responses.extend(
self.post_batch_request(calls=all_calls[i : i + batch_size])
)
Comment on lines +1132 to +1137

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Instead of sending RPC requests all at once, this PR batches them to reduce server load.


result_alloc: Dict[Address, Account | None] = {}
offset = 0
Expand Down
70 changes: 70 additions & 0 deletions packages/testing/src/execution_testing/specs/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,72 @@ def _split_blocks_by_phase(blocks: List[Block]) -> List[Block]:
return out


_SETUP_BLOCK_GAS_HEADROOM = 0.99


def _split_setup_blocks_by_gas(
blocks: List[Block], *, block_gas_limit: int
) -> List[Block]:
"""
Split each SETUP block into runs that fit one block's gas limit.

A live client inherits its gas limit from the snapshot chain and
cannot raise it, so setup work beyond one block's worth has to span
several or the client rejects the payload with ``gas limit reached``.
Execution blocks pass through at any size: one is the unit a
benchmark measures, so splitting it would change the result.

Requires single-phase blocks; run ``_split_blocks_by_phase`` first.
"""
budget = int(block_gas_limit * _SETUP_BLOCK_GAS_HEADROOM)
out: List[Block] = []
for block in blocks:
if not block.txs or block.phase is not TestPhase.SETUP:
out.append(block)
continue

runs: List[List[Transaction]] = []
current_run: List[Transaction] = []
current_gas = 0
for tx in block.txs:
if tx.gas_limit > block_gas_limit:
raise ValueError(
f"Setup tx gas_limit ({int(tx.gas_limit)}) exceeds "
f"the snapshot chain's block gas limit "
f"({block_gas_limit}); no split can make it fit."
)
if current_run and current_gas + tx.gas_limit > budget:
runs.append(current_run)
current_run, current_gas = [], 0
current_run.append(tx)
current_gas += tx.gas_limit
Comment thread
LouisTsai-Csie marked this conversation as resolved.
runs.append(current_run)

if len(runs) == 1:
out.append(block)
continue

out.extend(
block.model_copy(
update={
"txs": run_txs,
"header_verify": None,
"rlp_modifier": None,
"expected_block_access_list": None,
"expected_post_state": None,
"expected_gas_used": None,
"exception": None,
"skip_exception_verification": False,
"engine_api_error_code": None,
}
)
for run_txs in runs[:-1]
)
out.append(block.model_copy(update={"txs": runs[-1]}))

return out


class BlockchainTest(BaseTest):
"""Filler type that tests multiple blocks (valid or invalid) in a chain."""

Expand Down Expand Up @@ -1483,6 +1549,10 @@ def make_stateful_fixture(
# split into contiguous phase runs so benchmark gas isn't
# swallowed into ``setupEngineNewPayloads``.
blocks_to_process.extend(_split_blocks_by_phase(self.blocks))
blocks_to_process = _split_setup_blocks_by_gas(
blocks_to_process,
block_gas_limit=int(HexNumber(start_block["gasLimit"])),
)

# Chain off the session start_block. We pull parent_* from a
# FixtureHeader-validated copy of the client's block dict, but
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Test suite for ``_split_setup_blocks_by_gas`` in stateful filling."""

import pytest

from execution_testing.test_types import TestPhase, Transaction

from ..blockchain import (
_SETUP_BLOCK_GAS_HEADROOM,
Block,
_split_setup_blocks_by_gas,
)

BLOCK_GAS_LIMIT = 1_000_000
"""Stand-in for the snapshot chain's block gas limit."""

BUDGET = int(BLOCK_GAS_LIMIT * _SETUP_BLOCK_GAS_HEADROOM)


def _tx(
gas_limit: int, phase: TestPhase | None = TestPhase.SETUP
) -> Transaction:
"""Build a phase-tagged Transaction of the given gas limit."""
tx = Transaction(gas_limit=gas_limit)
tx.test_phase = phase
return tx


def _split(block: Block) -> list[Block]:
"""Split *block* against the stand-in block gas limit."""
return _split_setup_blocks_by_gas([block], block_gas_limit=BLOCK_GAS_LIMIT)


def test_passthrough_within_budget() -> None:
"""A setup block that already fits is returned unchanged."""
block = Block(txs=[_tx(100_000) for _ in range(5)])
out = _split(block)
assert len(out) == 1
assert out[0] is block


def test_passthrough_empty() -> None:
"""Empty-txs block is returned unchanged (identity)."""
block = Block(txs=[])
out = _split(block)
assert len(out) == 1
assert out[0] is block


def test_passthrough_all_untagged() -> None:
"""Block with no phase-tagged txs is returned unchanged."""
block = Block(txs=[_tx(BLOCK_GAS_LIMIT, phase=None) for _ in range(4)])
out = _split(block)
assert len(out) == 1
assert out[0] is block


def test_execution_block_never_split() -> None:
"""Execution block survives intact far past the block gas limit."""
block = Block(
txs=[
_tx(BLOCK_GAS_LIMIT, phase=TestPhase.EXECUTION) for _ in range(10)
]
)
out = _split(block)
assert len(out) == 1
assert out[0] is block


def test_every_sub_block_fits_the_budget() -> None:
"""Over-budget setup work spans blocks that each fit."""
out = _split(Block(txs=[_tx(200_000) for _ in range(25)]))
assert len(out) > 1
for block in out:
assert sum(tx.gas_limit for tx in block.txs) <= BUDGET


def test_split_preserves_every_tx_in_order() -> None:
"""Splitting neither drops, duplicates nor reorders transactions."""
txs = [_tx(200_000) for _ in range(25)]
out = _split(Block(txs=txs))
assert [tx for block in out for tx in block.txs] == txs


def test_tx_at_the_limit_gets_its_own_block() -> None:
"""A tx past the budget but within the limit is isolated, not dropped."""
small, huge = _tx(21_000), _tx(BLOCK_GAS_LIMIT)
out = _split(Block(txs=[small, huge, small]))
assert [len(block.txs) for block in out] == [1, 1, 1]
assert out[1].txs[0] is huge


def test_tx_past_the_limit_raises() -> None:
"""A tx no block can hold fails loudly instead of being emitted."""
block = Block(txs=[_tx(21_000), _tx(BLOCK_GAS_LIMIT * 3)])
with pytest.raises(ValueError, match="no split can make it fit"):
_split(block)


@pytest.mark.parametrize(
"gas_limits, expected_sizes",
[
([500_000, 400_000], [2]),
([500_000, 600_000], [1, 1]),
([900_000, 90_000], [2]),
([900_000, 100_000], [1, 1]),
([250_000] * 9, [3, 3, 3]),
([300_000] * 7, [3, 3, 1]),
([BLOCK_GAS_LIMIT // 2] * 2, [1, 1]),
],
)
def test_sub_block_sizes(
gas_limits: list[int], expected_sizes: list[int]
) -> None:
"""Sub-block sizes follow a greedy pack of the gas budget."""
block = Block(txs=[_tx(gas) for gas in gas_limits])
out = _split(block)
assert [len(block.txs) for block in out] == expected_sizes
6 changes: 4 additions & 2 deletions tests/benchmark/compute/instruction/test_account_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ def test_ext_account_query_cold(

creation_txs = []
with TestPhaseManager.setup():
creation_sender = pre.fund_eoa()
num_creation_txs = math.ceil(
num_target_accounts / max_creations_per_tx
)
Expand All @@ -257,7 +258,7 @@ def test_ext_account_query_cold(
to=factory_address,
data=Hash(addr_start),
gas_limit=tx_gas_limit,
sender=pre.fund_eoa(),
sender=creation_sender,
)
)
blocks.append(Block(txs=creation_txs))
Expand Down Expand Up @@ -285,6 +286,7 @@ def test_ext_account_query_cold(

execution_txs = []
with TestPhaseManager.execution():
execution_sender = pre.fund_eoa()
max_target_per_tx = (
tx_gas_limit - intrinsic_gas_cost_calc()
) // cold_account_access_gas
Expand All @@ -304,7 +306,7 @@ def test_ext_account_query_cold(
to=op_address,
data=calldata,
gas_limit=gas_limit,
sender=pre.fund_eoa(),
sender=execution_sender,
)
)
gas_used += gas_limit
Expand Down
5 changes: 3 additions & 2 deletions tests/benchmark/compute/scenario/test_transaction_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,14 +295,15 @@ def test_ether_transfers_to_precompile(
recipient_type=RecipientType.PRECOMPILE,
)
iteration_count = gas_benchmark_value // iteration_cost
sender = pre.fund_eoa()
txs = []
for _ in range(iteration_count):
txs.append(
Transaction(
to=Address(precompile),
value=transfer_amount,
gas_limit=iteration_cost,
sender=pre.fund_eoa(),
sender=sender,
)
)

Expand Down Expand Up @@ -406,7 +407,7 @@ def test_block_full_data(

txs.append(
Transaction(
to=pre.fund_eoa(),
to=pre.fund_eoa(amount=1),
data=data,
gas_limit=gas_available + intrinsic_cost,
sender=pre.fund_eoa(),
Expand Down
1 change: 1 addition & 0 deletions tests/benchmark/stateful/bloatnet/test_account_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ def test_account_access(
opcode(
address=address_source.address_op(),
value=value_sent,
gas=Op.GAS,
# Gas accounting
address_warm=access_warm,
value_transfer=value_sent > 0,
Expand Down
13 changes: 7 additions & 6 deletions tests/benchmark/stateful/bloatnet/test_transaction_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def test_ether_transfers_onchain_receivers(
raise ValueError(f"Unknown case: {case_id}")

sends_value = transfer_amount > 0
iteration_cost = (
regular_cost = (
fork.transaction_intrinsic_cost_calculator()(
sends_value=sends_value,
recipient_type=recipient_type,
Expand All @@ -164,13 +164,14 @@ def test_ether_transfers_onchain_receivers(
sends_value=sends_value,
recipient_type=recipient_type,
)
+ fork.transaction_top_frame_state_gas(
sends_value=sends_value,
recipient_type=recipient_type,
)
+ receiver_execution_gas
)
iteration_count = gas_benchmark_value // iteration_cost
state_cost = fork.transaction_top_frame_state_gas(
sends_value=sends_value,
recipient_type=recipient_type,
)
iteration_cost = regular_cost + state_cost
iteration_count = gas_benchmark_value // max(regular_cost, state_cost)
Comment on lines +169 to +174

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the actual worst case for ether transfer, the iteration cost is sum(R, S), while the gas usage in the header is max(R, S), not sum(R, S).

The original approach used ~90% of the block gas limit, but now it reaches 99%.


txs = []
for _ in range(iteration_count):
Expand Down
Loading