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
2 changes: 1 addition & 1 deletion tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ class ReferenceSpec:

ref_spec_2780 = ReferenceSpec(
git_path="EIPS/eip-2780.md",
version="4b612eec2ef70611bba3e0819d137dcfb9b6cd81",
version="992074053f12f24fed9e6d6bf6099d3a44707dca",
)
153 changes: 153 additions & 0 deletions tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_calldata_floor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""
EIP-2780 interaction with the EIP-7623/7976 calldata floor.

"""

import pytest
from execution_testing import (
Account,
Address,
Alloc,
Bytes,
Fork,
RecipientType,
StateTestFiller,
Transaction,
TransactionException,
)

from ...prague.eip7623_increase_calldata_cost.helpers import (
find_floor_cost_threshold,
)
from .helpers import EOA_INITIAL_BALANCE
from .spec import ref_spec_2780

REFERENCE_SPEC_GIT_PATH = ref_spec_2780.git_path
REFERENCE_SPEC_VERSION = ref_spec_2780.version

pytestmark = pytest.mark.valid_from("Amsterdam")


def _floor_dominating_calldata(fork: Fork) -> Bytes:
"""
Return zero-byte calldata sized so its calldata floor strictly
exceeds the decomposed value-transfer intrinsic for a non-create
call to an existing EOA.

Reuses the shared EIP-7623 ``find_floor_cost_threshold`` binary
search against this transaction shape, then steps one byte past the
threshold (the last size where the floor does not yet dominate) so
the floor strictly binds.
"""
intrinsic_calc = fork.transaction_intrinsic_cost_calculator()
floor_calc = fork.transaction_data_floor_cost_calculator()

def intrinsic(byte_count: int) -> int:
return intrinsic_calc(
calldata=b"\x00" * byte_count,
sends_value=True,
recipient_type=RecipientType.EOA,
return_cost_deducted_prior_execution=True,
)

def floor(byte_count: int) -> int:
return floor_calc(data=b"\x00" * byte_count)

threshold = find_floor_cost_threshold(
floor_data_gas_cost_calculator=floor,
intrinsic_gas_cost_calculator=intrinsic,
)
byte_count = threshold + 1

assert floor(byte_count) > intrinsic(byte_count)
return Bytes(b"\x00" * byte_count)


@pytest.mark.parametrize(
"gas_modifier",
[
pytest.param(0, id="at_floor"),
pytest.param(
-1,
id="below_floor",
marks=pytest.mark.exception_test,
),
],
)
Comment thread
gurukamath marked this conversation as resolved.
@pytest.mark.parametrize(
"value",
[
pytest.param(0, id="zero_value"),
pytest.param(1, id="non-zero_value"),
],
)
def test_calldata_floor(
fork: Fork,
pre: Alloc,
state_test: StateTestFiller,
gas_modifier: int,
value: int,
) -> None:
"""
A data-heavy transaction to an existing EOA whose calldata floor
exceeds the decomposed value-transfer intrinsic.

- ``floor_binds``: with a gas limit above the floor, ``gas_used``
pins to the floor, so the value-transfer charges
(``TRANSFER_LOG_COST + TX_VALUE_COST``) folded into the intrinsic
are masked -- the gas paid is identical at ``value == 0`` and
``value == 1`` and only the moved wei differs.
- ``below_floor``: a gas limit one short of the floor still covers
the (smaller) decomposed intrinsic, so the floor -- built on the
EIP-2780-lowered ``TX_BASE`` -- is the only thing that can reject
it, with ``INTRINSIC_GAS_BELOW_FLOOR_GAS_COST``.
"""
sender_initial_balance = 10**18
sender = pre.fund_eoa(sender_initial_balance)
target = pre.fund_eoa(amount=EOA_INITIAL_BALANCE)

calldata = _floor_dominating_calldata(fork)
calldata_floor = fork.transaction_data_floor_cost_calculator()(
data=calldata,
)
gas_price = 1_000_000_000

post: dict[Address, Account] = {}
gas_limit = calldata_floor + gas_modifier
# Even at the reduced limit the decomposed intrinsic is still
# covered, so the calldata floor is the sole gate on the
# transaction.
intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(
calldata=calldata,
sends_value=bool(value),
recipient_type=RecipientType.EOA,
return_cost_deducted_prior_execution=True,
)
assert intrinsic_gas <= gas_limit, (
"gas_limit must still cover the decomposed intrinsic so the "
"outcome is pinned to the calldata floor"
)

tx = Transaction(
sender=sender,
to=target,
value=value,
data=calldata,
gas_limit=gas_limit,
gas_price=gas_price,
error=(
TransactionException.INTRINSIC_GAS_BELOW_FLOOR_GAS_COST
if gas_modifier < 0
else None
),
)
if gas_modifier == 0:
sender_final_balance = (
sender_initial_balance - value - calldata_floor * gas_price
)
post = {
sender: Account(nonce=1, balance=sender_final_balance),
target: Account(balance=EOA_INITIAL_BALANCE + value),
}

state_test(pre=pre, tx=tx, post=post)
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""
Fork-transition tests for EIP-2780.

EIP-2780 reshapes the intrinsic transaction cost at the Amsterdam fork
boundary. These tests send identical transactions in a pre-fork block
and a post-fork block (straddling the transition timestamp) and assert
that the per-transaction gas paid changes by the EIP-2780 amount only
once the fork activates.

For these shapes the post-fork intrinsic decomposes from the flat
pre-fork ``TX_BASE`` of 21_000 as follows:

- A plain call to an existing account drops to ``TX_BASE`` (12_000)
plus the new ``COLD_ACCOUNT_ACCESS`` recipient charge; adding value
re-raises it to exactly 21_000 (the value-transfer cost is invariant
across the fork by design).
- A self-transfer is fully carved out post-fork: it pays only the
lowered ``TX_BASE`` with no recipient or value-transfer charge,
regardless of value, the largest reduction.
"""

import pytest
from execution_testing import (
Account,
Alloc,
Block,
BlockchainTestFiller,
RecipientType,
Transaction,
TransitionFork,
)

from .helpers import EOA_INITIAL_BALANCE
from .spec import ref_spec_2780

REFERENCE_SPEC_GIT_PATH = ref_spec_2780.git_path
REFERENCE_SPEC_VERSION = ref_spec_2780.version

pytestmark = pytest.mark.valid_at_transition_to("Amsterdam")

# Transition forks switch at timestamp 15_000.
PRE_FORK_TIMESTAMP = 14_999
POST_FORK_TIMESTAMP = 15_000


@pytest.mark.parametrize(
"self_transfer",
[
pytest.param(False, id="plain_call"),
pytest.param(True, id="self_transfer"),
],
)
@pytest.mark.parametrize(
"value",
[
pytest.param(0, id="zero_value"),
pytest.param(1, id="non-zero_value"),
],
)
def test_intrinsic_reduction_across_amsterdam_transition(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: TransitionFork,
self_transfer: bool,
value: int,
) -> None:
"""
Pin the EIP-2780 intrinsic change across the Amsterdam boundary.

The same transaction shape is sent in a pre-fork block (Osaka
rules, flat 21_000 intrinsic) and a post-fork block (Amsterdam
rules, decomposed intrinsic). Each block uses a distinct sender so
its post-tx balance pins the fork-appropriate intrinsic; the
recipient is an existing EOA (or the sender itself for
``self_transfer``), so neither block runs EVM bytecode and
``gas_used`` equals the intrinsic exactly.

The per-fork intrinsic returned by the calculator is also checked
against a hand-derived decomposition built from each fork's gas
constants, so a calculator regression fails here with a clear
message rather than only as a downstream balance mismatch.
"""
gas_price = 10**9
recipient_type = RecipientType.SELF if self_transfer else RecipientType.EOA

pre_fork = fork.fork_at(timestamp=PRE_FORK_TIMESTAMP)
post_fork = fork.fork_at(timestamp=POST_FORK_TIMESTAMP)

# Pre-fork: flat ``TX_BASE`` regardless of recipient kind or value.
expected_pre = pre_fork.gas_costs().TX_BASE
# Post-fork: EIP-2780 decomposition. Self-transfers are fully
# carved out; other recipients pay the recipient access charge plus
# the value-transfer charges when value is moved.
post_gas_costs = post_fork.gas_costs()
expected_post = post_gas_costs.TX_BASE
if not self_transfer:
expected_post += post_gas_costs.COLD_ACCOUNT_ACCESS
if value:
expected_post += (
post_gas_costs.TRANSFER_LOG_COST + post_gas_costs.TX_VALUE_COST
)

timestamps = [PRE_FORK_TIMESTAMP, POST_FORK_TIMESTAMP]
expected_intrinsics = [expected_pre, expected_post]
blocks = []
post: dict = {}

for timestamp, expected_intrinsic in zip(
timestamps, expected_intrinsics, strict=True
):
sub_fork = fork.fork_at(timestamp=timestamp)
intrinsic_gas = sub_fork.transaction_intrinsic_cost_calculator()(
sends_value=bool(value),
recipient_type=recipient_type,
return_cost_deducted_prior_execution=True,
)
assert intrinsic_gas == expected_intrinsic, (
f"intrinsic at timestamp {timestamp} ({sub_fork}) is "
f"{intrinsic_gas}, expected {expected_intrinsic}"
)

sender_initial_balance = 10**18
sender = pre.fund_eoa(sender_initial_balance)
if self_transfer:
target = sender
else:
target = pre.fund_eoa(amount=EOA_INITIAL_BALANCE)

# No EVM bytecode runs (recipient is an EOA or the sender), so
# gas_used == intrinsic_gas; the gas limit is pinned to exactly
# the intrinsic, leaving no buffer.
tx = Transaction(
sender=sender,
to=target,
value=value,
gas_limit=intrinsic_gas,
gas_price=gas_price,
)
blocks.append(Block(timestamp=timestamp, txs=[tx]))

# A self-transfer returns the value to the sender (net zero);
# a plain call moves ``value`` to the distinct recipient.
sender_value_delta = 0 if self_transfer else value
sender_final_balance = (
sender_initial_balance
- sender_value_delta
- intrinsic_gas * gas_price
)
post[sender] = Account(nonce=1, balance=sender_final_balance)
if not self_transfer:
post[target] = Account(balance=EOA_INITIAL_BALANCE + value)

blockchain_test(pre=pre, blocks=blocks, post=post)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from execution_testing import (
Alloc,
Fork,
Op,
RecipientType,
StateTestFiller,
Transaction,
Expand Down Expand Up @@ -67,3 +68,48 @@ def test_intrinsic_gas_floor_boundary(
)

state_test(pre=pre, tx=tx, post={})


@pytest.mark.exception_test
@pytest.mark.parametrize(
"value",
[
pytest.param(0, id="zero_value"),
pytest.param(1, id="non-zero_value"),
],
)
def test_intrinsic_gas_floor_boundary_contract_creation(
fork: Fork,
pre: Alloc,
state_test: StateTestFiller,
value: int,
) -> None:
"""
Reject a contract-creation transaction when
``gas_limit = intrinsic_gas - 1``.

A creation tx's intrinsic includes the ``NEW_ACCOUNT`` state gas, so
the pre-execution check rejects against the combined
``regular + state`` intrinsic. The init code never runs.
"""
sender = pre.fund_eoa(10**18)
init_code = Op.STOP

intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(
calldata=init_code,
contract_creation=True,
sends_value=bool(value),
return_cost_deducted_prior_execution=True,
)

tx = Transaction(
sender=sender,
to=None,
value=value,
data=init_code,
gas_limit=intrinsic_gas - 1,
gas_price=1_000_000_000,
error=TransactionException.INTRINSIC_GAS_TOO_LOW,
)

state_test(pre=pre, tx=tx, post={})
Loading
Loading