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
4 changes: 4 additions & 0 deletions .markdownlint-cli2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ignores:
- EIP8037_REMAINING_FAILURES.md
- EIP8037_PORTED_STATIC_FAILURES.md
- EIP8037_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -595,21 +595,36 @@ def _fund_eoa(
# Send a transaction to fund the EOA
fund_tx: PendingTransaction | None = None
if delegation is not None or storage is not None:
fork = self._fork.fork_at(
block_number=self._block_number, timestamp=self._timestamp
)
intrinsic_calc = fork.transaction_intrinsic_cost_calculator()

if storage is not None:
if not isinstance(storage, Storage):
storage = Storage.model_validate(storage)
logger.debug(
f"Deploying storage contract for EOA {eoa} "
f"with {len(storage)} storage slots"
)
sstore_address = self.deploy_contract(
code=(
sum(
Op.SSTORE(key, value)
for key, value in storage.items()

storage_init_code = (
sum(
Op.SSTORE(
key,
value,
# gas accounting
key_warm=False,
original_value=0,
current_value=0,
new_value=1,
)
+ Op.STOP
for key, value in storage.items()
)
+ Op.STOP
)
sstore_address = self.deploy_contract(
code=storage_init_code,
)
logger.debug(
f"Storage contract deployed at {sstore_address} "
Expand All @@ -629,7 +644,11 @@ def _fund_eoa(
signer=eoa,
),
],
gas_limit=100_000,
gas_limit=(
intrinsic_calc(authorization_list_or_count=1)
+ storage_init_code.gas_cost(fork)
+ 500_000
),
Comment on lines +647 to +651
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Remove the hardcoded gas limit, adjust to dynamic gas cost calculation and leave some buffer.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This applies to other changes within the file.

)
eoa.nonce = Number(eoa.nonce + 1)

Expand All @@ -654,7 +673,7 @@ def _fund_eoa(
signer=eoa,
),
],
gas_limit=100_000,
gas_limit=(intrinsic_calc(authorization_list_or_count=1)),
)
eoa.nonce = Number(eoa.nonce + 1)
else:
Expand All @@ -672,7 +691,7 @@ def _fund_eoa(
signer=eoa,
),
],
gas_limit=100_000,
gas_limit=intrinsic_calc(authorization_list_or_count=1),
)
eoa.nonce = Number(eoa.nonce + 1)

Expand Down
35 changes: 35 additions & 0 deletions packages/testing/src/execution_testing/specs/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
FixtureBlockBase,
FixtureConfig,
FixtureEngineNewPayload,
FixtureExecutionPayloadModifier,
FixtureHeader,
FixtureTransaction,
FixtureWithdrawal,
Expand Down Expand Up @@ -395,6 +396,7 @@ class BuiltBlock(CamelModel):
result: Result
expected_exception: BLOCK_EXCEPTION_TYPE = None
engine_api_error_code: EngineAPIError | None = None
rlp_modifier: Header | None = None
fork: Fork
block_access_list: BlockAccessList | None

Expand Down Expand Up @@ -447,6 +449,36 @@ def get_block_rlp(self) -> Bytes:
"""Get the RLP of the block."""
return self.get_fixture_block().rlp

@staticmethod
def derive_engine_payload_modifier(
rlp_modifier: Header | None,
block_access_list: BlockAccessList | None,
) -> "FixtureExecutionPayloadModifier | None":
"""
Propagate ``rlp_modifier``'s header changes to the engine payload.

The engine ``ExecutionPayload`` schema does not carry
``block_access_list_hash`` directly; the equivalent payload field is
the ``block_access_list`` body. So a header modifier that touches the
BAL hash needs to drive a matching change on the payload body.
"""
if rlp_modifier is None:
return None
bal_hash_override = rlp_modifier.block_access_list_hash
if bal_hash_override is None:
return None
if bal_hash_override is Header.REMOVE_FIELD:
return FixtureExecutionPayloadModifier(
block_access_list=(
FixtureExecutionPayloadModifier.REMOVE_FIELD
),
)
if block_access_list is None:
return FixtureExecutionPayloadModifier(
block_access_list=Bytes(b""),
)
return None

def get_fixture_engine_new_payload(self) -> FixtureEngineNewPayload:
"""Get a FixtureEngineNewPayload from the built block."""
return FixtureEngineNewPayload.from_fixture_header(
Expand All @@ -458,6 +490,9 @@ def get_fixture_engine_new_payload(self) -> FixtureEngineNewPayload:
block_access_list=self.block_access_list.rlp
if self.block_access_list
else None,
execution_payload_modifier=self.derive_engine_payload_modifier(
self.rlp_modifier, self.block_access_list
),
validation_error=self.expected_exception,
error_code=self.engine_api_error_code,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,8 @@ def test_tx_iterations_by_total_iteration_count_raises_on_impossible() -> None:

with pytest.raises(
ValueError,
match="Single iteration gas cost is greater than gas limit.",
match="Single iteration gas cost exceeds gas_limit "
"or compute_gas_limit.",
):
list(
bytecode.tx_iterations_by_total_iteration_count(
Expand Down
104 changes: 66 additions & 38 deletions packages/testing/src/execution_testing/tools/tools_code/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,10 @@ class IteratingBytecode(Bytecode):
"""
cleanup: Bytecode
"""Bytecode executed once at the end after all iterations complete."""
iterating_state_gas: int
"""
State-gas portion (EIP-8037) charged per loop iteration.
"""

def __new__(
cls,
Expand All @@ -815,6 +819,7 @@ def __new__(
cleanup: Bytecode | None = None,
warm_iterating: Bytecode | None = None,
iterating_subcall: Bytecode | int | None = None,
iterating_state_gas: int = 0,
) -> Self:
"""
Create a new iterating bytecode.
Expand All @@ -833,6 +838,8 @@ def __new__(
calculation. The value can also be an integer, in which case it
represents the gas cost of the subcall (e.g. the subcall is a
precompiled contract).
iterating_state_gas: EIP-8037 state-gas portion charged
per iteration, defaults to 0.

Returns:
A new IteratingBytecode instance.
Expand Down Expand Up @@ -860,6 +867,7 @@ def __new__(
if cleanup is None:
cleanup = Bytecode()
instance.cleanup = cleanup
instance.iterating_state_gas = iterating_state_gas
return instance

def iterating_subcall_gas_cost(
Expand Down Expand Up @@ -985,60 +993,85 @@ def tx_gas_limit_by_iteration_count(
**intrinsic_cost_kwargs,
) + self.iterating_subcall_reserve(fork=fork)

def _iterations_fit_within_gas_limits(
self,
*,
fork: Fork,
iteration_count: int,
start_iteration: int,
gas_limit: int,
compute_gas_limit: int | None = None,
**intrinsic_cost_kwargs: Any,
) -> bool:
"""
Check whether iteration_count iterations fit within the gas limits.

Returns True when both:
- The combined regular+state gas (i.e. tx.gas) is <=
gas_limit (block-budget constraint).
- The regular gas, computed as
combined - iteration_count * iterating_state_gas,
respects the compute_gas_limit.
"""
if iteration_count <= 0:
return True
combined = self.tx_gas_limit_by_iteration_count(
fork=fork,
iteration_count=iteration_count,
start_iteration=start_iteration,
**intrinsic_cost_kwargs,
)
if combined > gas_limit:
return False
if compute_gas_limit is not None:
compute = combined - iteration_count * self.iterating_state_gas
if compute > compute_gas_limit:
return False
return True

def _binary_search_iterations(
self,
*,
fork: Fork,
gas_limit: int,
start_iteration: int,
compute_gas_limit: int | None = None,
**intrinsic_cost_kwargs: Any,
) -> Tuple[int, int]:
"""
Binary search for the maximum iterations that fit within a gas limit.
"""
single_iteration_gas = self.tx_gas_limit_by_iteration_count(
fork=fork,
iteration_count=1,
start_iteration=start_iteration,
fits_kwargs: Dict[str, Any] = {
"fork": fork,
"start_iteration": start_iteration,
"gas_limit": gas_limit,
"compute_gas_limit": compute_gas_limit,
**intrinsic_cost_kwargs,
)
if single_iteration_gas > gas_limit:
}

if not self._iterations_fit_within_gas_limits(
iteration_count=1, **fits_kwargs
):
raise ValueError(
"Single iteration gas cost is greater than gas limit."
"Single iteration gas cost exceeds gas_limit "
"or compute_gas_limit."
)

low = 1
high = 2

# Exponential search to find upper bound
high_gas_cost = self.tx_gas_limit_by_iteration_count(
fork=fork,
iteration_count=high,
start_iteration=start_iteration,
**intrinsic_cost_kwargs,
)
while high_gas_cost < gas_limit:
while self._iterations_fit_within_gas_limits(
iteration_count=high, **fits_kwargs
):
low = high
high *= 2
high_gas_cost = self.tx_gas_limit_by_iteration_count(
fork=fork,
iteration_count=high,
start_iteration=start_iteration,
**intrinsic_cost_kwargs,
)

# Binary search for exact fit
best_iterations = 0
while low < high:
mid = (low + high) // 2

if (
self.tx_gas_limit_by_iteration_count(
fork=fork,
iteration_count=mid,
start_iteration=start_iteration,
**intrinsic_cost_kwargs,
)
> gas_limit
if not self._iterations_fit_within_gas_limits(
iteration_count=mid, **fits_kwargs
):
high = mid
else:
Expand Down Expand Up @@ -1082,17 +1115,11 @@ def tx_iterations_by_gas_limit(
start_iteration=start_iteration,
**intrinsic_cost_kwargs,
):
# Binary search for the maximum number of iterations that fits
# within remaining_gas
max_gas_limit = (
min(remaining_gas, gas_limit_cap)
if gas_limit_cap is not None
else remaining_gas
)
best_iterations, best_iterations_gas = (
self._binary_search_iterations(
fork=fork,
gas_limit=max_gas_limit,
gas_limit=remaining_gas,
compute_gas_limit=gas_limit_cap,
start_iteration=start_iteration,
**intrinsic_cost_kwargs,
)
Expand Down Expand Up @@ -1142,6 +1169,7 @@ def tx_iterations_by_total_iteration_count(
best_iterations, _ = self._binary_search_iterations(
fork=fork,
gas_limit=gas_limit_cap,
compute_gas_limit=gas_limit_cap,
start_iteration=start_iteration,
**intrinsic_cost_kwargs,
)
Expand Down
7 changes: 0 additions & 7 deletions src/ethereum/forks/amsterdam/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,13 +257,6 @@ class Header:
[EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928
[cbalh]: ref:ethereum.forks.amsterdam.block_access_lists.hash_block_access_list
""" # noqa: E501
slot_number: U64
"""
The slot number of this block as provided by the consensus layer.
Introduced in [EIP-7843].

[EIP-7843]: https://eips.ethereum.org/EIPS/eip-7843
"""

slot_number: U64
"""
Expand Down
Loading