Skip to content

Commit

Permalink
feat(forks,tests): Add EIP-7623 (#1004)
Browse files Browse the repository at this point in the history
* feat(forks): Add EIP-7623 logic to gas calculation

* fix(tests): Broken tests due to gas cost change

* feat(forks): Add `transaction_data_floor_cost_calculator`

* new(tests): EIP-7623: Add transaction validity tests

* review comments

* new(tests): EIP-7623: Add gas consumption tests

* refactor(tests): EIP-7623: minor refactor

* Update src/ethereum_test_forks/base_fork.py

Co-authored-by: Stuart Reed <stucan@gmail.com>

* Update src/ethereum_test_forks/base_fork.py

* Apply suggestions from code review

Co-authored-by: danceratopz <danceratopz@gmail.com>

* fix(forks): tox

* docs: changelog

---------

Co-authored-by: Stuart Reed <stucan@gmail.com>
Co-authored-by: danceratopz <danceratopz@gmail.com>
  • Loading branch information
3 people authored Dec 18, 2024
1 parent b820167 commit 6446ed4
Show file tree
Hide file tree
Showing 13 changed files with 1,162 additions and 10 deletions.
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Test fixtures for use by clients are available for each release on the [Github r
-[EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) many delegations test ([#923](https://github.com/ethereum/execution-spec-tests/pull/923))
-[EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) set code of non-empty-storage account test ([#948](https://github.com/ethereum/execution-spec-tests/pull/948))
-[EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) Remove delegation behavior of EXTCODE* ([#984](https://github.com/ethereum/execution-spec-tests/pull/984))
-[EIP-7623](https://eips.ethereum.org/EIPS/eip-7623) Increase calldata cost ([#1004](https://github.com/ethereum/execution-spec-tests/pull/1004))

### 🛠️ Framework

Expand Down
37 changes: 37 additions & 0 deletions src/ethereum_test_forks/base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ class CalldataGasCalculator(Protocol):
A protocol to calculate the transaction gas cost of calldata for a given fork.
"""

def __call__(self, *, data: BytesConvertible, floor: bool = False) -> int:
"""
Returns the transaction gas cost of calldata given its contents.
"""
pass


class TransactionDataFloorCostCalculator(Protocol):
"""
A protocol to calculate the transaction floor cost due to its calldata for a given fork.
"""

def __call__(self, *, data: BytesConvertible) -> int:
"""
Returns the transaction gas cost of calldata given its contents.
Expand All @@ -63,9 +75,24 @@ def __call__(
contract_creation: bool = False,
access_list: List[AccessList] | None = None,
authorization_list_or_count: Sized | int | None = None,
return_cost_deducted_prior_execution: bool = False,
) -> int:
"""
Returns the intrinsic gas cost of a transaction given its properties.
Args:
calldata: The data of the transaction.
contract_creation: Whether the transaction creates a contract.
access_list: The list of access lists for the transaction.
authorization_list_or_count: The list of authorizations or the count of authorizations
for the transaction.
return_cost_deducted_prior_execution: If set to False, the returned value is equal to
the minimum gas required for the transaction to be valid. If set to True, the
returned value is equal to the cost that is deducted from the gas limit before
the transaction starts execution.
Returns:
Gas cost of a transaction
"""
pass

Expand Down Expand Up @@ -236,6 +263,16 @@ def calldata_gas_calculator(
"""
pass

@classmethod
@abstractmethod
def transaction_data_floor_cost_calculator(
cls, block_number: int = 0, timestamp: int = 0
) -> TransactionDataFloorCostCalculator:
"""
Returns a callable that calculates the transaction floor cost due to its calldata.
"""
pass

@classmethod
@abstractmethod
def transaction_intrinsic_cost_calculator(
Expand Down
84 changes: 81 additions & 3 deletions src/ethereum_test_forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
BaseFork,
CalldataGasCalculator,
MemoryExpansionGasCalculator,
TransactionDataFloorCostCalculator,
TransactionIntrinsicCostCalculator,
)
from ..gas_costs import GasCosts
Expand Down Expand Up @@ -140,6 +141,8 @@ def gas_costs(cls, block_number: int = 0, timestamp: int = 0) -> GasCosts:
G_MEMORY=3,
G_TX_DATA_ZERO=4,
G_TX_DATA_NON_ZERO=68,
G_TX_DATA_STANDARD_TOKEN_COST=0,
G_TX_DATA_FLOOR_TOKEN_COST=0,
G_TRANSACTION=21_000,
G_TRANSACTION_CREATE=32_000,
G_LOG=375,
Expand Down Expand Up @@ -184,7 +187,7 @@ def calldata_gas_calculator(
"""
gas_costs = cls.gas_costs(block_number, timestamp)

def fn(*, data: BytesConvertible) -> int:
def fn(*, data: BytesConvertible, floor: bool = False) -> int:
cost = 0
for b in Bytes(data):
if b == 0:
Expand All @@ -195,6 +198,19 @@ def fn(*, data: BytesConvertible) -> int:

return fn

@classmethod
def transaction_data_floor_cost_calculator(
cls, block_number: int = 0, timestamp: int = 0
) -> TransactionDataFloorCostCalculator:
"""
At frontier, the transaction data floor cost is a constant zero.
"""

def fn(*, data: BytesConvertible) -> int:
return 0

return fn

@classmethod
def transaction_intrinsic_cost_calculator(
cls, block_number: int = 0, timestamp: int = 0
Expand All @@ -211,6 +227,7 @@ def fn(
contract_creation: bool = False,
access_list: List[AccessList] | None = None,
authorization_list_or_count: Sized | int | None = None,
return_cost_deducted_prior_execution: bool = False,
) -> int:
assert access_list is None, f"Access list is not supported in {cls.name()}"
assert (
Expand Down Expand Up @@ -633,6 +650,7 @@ def fn(
contract_creation: bool = False,
access_list: List[AccessList] | None = None,
authorization_list_or_count: Sized | int | None = None,
return_cost_deducted_prior_execution: bool = False,
) -> int:
intrinsic_cost: int = super_fn(
calldata=calldata,
Expand Down Expand Up @@ -764,7 +782,7 @@ def valid_opcodes(
@classmethod
def gas_costs(cls, block_number: int = 0, timestamp: int = 0) -> GasCosts:
"""
Returns a dataclass with the defined gas costs constants for genesis.
On Istanbul, the non-zero transaction data byte cost is reduced to 16 due to EIP-2028.
"""
return replace(
super(Istanbul, cls).gas_costs(block_number, timestamp),
Expand Down Expand Up @@ -818,6 +836,7 @@ def fn(
contract_creation: bool = False,
access_list: List[AccessList] | None = None,
authorization_list_or_count: Sized | int | None = None,
return_cost_deducted_prior_execution: bool = False,
) -> int:
intrinsic_cost: int = super_fn(
calldata=calldata,
Expand Down Expand Up @@ -1127,6 +1146,17 @@ def precompiles(cls, block_number: int = 0, timestamp: int = 0) -> List[Address]
block_number, timestamp
)

@classmethod
def gas_costs(cls, block_number: int = 0, timestamp: int = 0) -> GasCosts:
"""
On Prague, the standard token cost and the floor token costs are introduced due to EIP-7623
"""
return replace(
super(Prague, cls).gas_costs(block_number, timestamp),
G_TX_DATA_STANDARD_TOKEN_COST=4, # https://eips.ethereum.org/EIPS/eip-7623
G_TX_DATA_FLOOR_TOKEN_COST=10,
)

@classmethod
def system_contracts(cls, block_number: int = 0, timestamp: int = 0) -> List[Address]:
"""
Expand All @@ -1146,6 +1176,44 @@ def max_request_type(cls, block_number: int = 0, timestamp: int = 0) -> int:
"""
return 2

@classmethod
def calldata_gas_calculator(
cls, block_number: int = 0, timestamp: int = 0
) -> CalldataGasCalculator:
"""
Returns a callable that calculates the transaction gas cost for its calldata
depending on its contents.
"""
gas_costs = cls.gas_costs(block_number, timestamp)

def fn(*, data: BytesConvertible, floor: bool = False) -> int:
tokens = 0
for b in Bytes(data):
if b == 0:
tokens += 1
else:
tokens += 4
if floor:
return tokens * gas_costs.G_TX_DATA_FLOOR_TOKEN_COST
return tokens * gas_costs.G_TX_DATA_STANDARD_TOKEN_COST

return fn

@classmethod
def transaction_data_floor_cost_calculator(
cls, block_number: int = 0, timestamp: int = 0
) -> TransactionDataFloorCostCalculator:
"""
Starting in Prague, due to EIP-7623, the transaction data floor cost is introduced.
"""
calldata_gas_calculator = cls.calldata_gas_calculator(block_number, timestamp)
gas_costs = cls.gas_costs(block_number, timestamp)

def fn(*, data: BytesConvertible) -> int:
return calldata_gas_calculator(data=data, floor=True) + gas_costs.G_TRANSACTION

return fn

@classmethod
def transaction_intrinsic_cost_calculator(
cls, block_number: int = 0, timestamp: int = 0
Expand All @@ -1157,24 +1225,34 @@ def transaction_intrinsic_cost_calculator(
block_number, timestamp
)
gas_costs = cls.gas_costs(block_number, timestamp)
transaction_data_floor_cost_calculator = cls.transaction_data_floor_cost_calculator(
block_number, timestamp
)

def fn(
*,
calldata: BytesConvertible = b"",
contract_creation: bool = False,
access_list: List[AccessList] | None = None,
authorization_list_or_count: Sized | int | None = None,
return_cost_deducted_prior_execution: bool = False,
) -> int:
intrinsic_cost: int = super_fn(
calldata=calldata,
contract_creation=contract_creation,
access_list=access_list,
return_cost_deducted_prior_execution=False,
)
if authorization_list_or_count is not None:
if isinstance(authorization_list_or_count, Sized):
authorization_list_or_count = len(authorization_list_or_count)
intrinsic_cost += authorization_list_or_count * gas_costs.G_AUTHORIZATION
return intrinsic_cost

if return_cost_deducted_prior_execution:
return intrinsic_cost

transaction_floor_data_cost = transaction_data_floor_cost_calculator(data=calldata)
return max(intrinsic_cost, transaction_floor_data_cost)

return fn

Expand Down
2 changes: 2 additions & 0 deletions src/ethereum_test_forks/gas_costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class GasCosts:

G_TX_DATA_ZERO: int
G_TX_DATA_NON_ZERO: int
G_TX_DATA_STANDARD_TOKEN_COST: int
G_TX_DATA_FLOOR_TOKEN_COST: int

G_TRANSACTION: int
G_TRANSACTION_CREATE: int
Expand Down
17 changes: 14 additions & 3 deletions tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from ethereum_test_forks import Fork
from ethereum_test_tools import (
EOA,
AccessList,
Account,
Address,
Alloc,
Expand Down Expand Up @@ -559,9 +560,15 @@ def test_tx_entry_point(
start_balance = 10**18
sender = pre.fund_eoa(amount=start_balance)

# Starting from EIP-7623, we need to use an access list to raise the intrinsic gas cost to be
# above the floor data cost.
access_list = [AccessList(address=Address(i), storage_keys=[]) for i in range(1, 10)]

# Gas is appended the intrinsic gas cost of the transaction
tx_intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator()
intrinsic_gas_cost = tx_intrinsic_gas_cost_calculator(calldata=precompile_input)
intrinsic_gas_cost = tx_intrinsic_gas_cost_calculator(
calldata=precompile_input, access_list=access_list
)

# Consumed gas will only be the precompile gas if the proof is correct and
# the call gas is sufficient.
Expand All @@ -570,13 +577,17 @@ def test_tx_entry_point(
Spec.POINT_EVALUATION_PRECOMPILE_GAS
if call_gas >= Spec.POINT_EVALUATION_PRECOMPILE_GAS and proof_correct
else call_gas
) + intrinsic_gas_cost

) + tx_intrinsic_gas_cost_calculator(
calldata=precompile_input,
access_list=access_list,
return_cost_deducted_prior_execution=True,
)
fee_per_gas = 7

tx = Transaction(
sender=sender,
data=precompile_input,
access_list=access_list,
to=Address(Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS),
gas_limit=call_gas + intrinsic_gas_cost,
gas_price=fee_per_gas,
Expand Down
19 changes: 16 additions & 3 deletions tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
""" # noqa: E501
import itertools
from typing import Mapping
from typing import List, Mapping

import pytest

from ethereum_test_forks import Fork
from ethereum_test_tools import Account, Address, Alloc, Bytecode, Environment
from ethereum_test_tools import AccessList, Account, Address, Alloc, Bytecode, Environment
from ethereum_test_tools import Opcodes as Op
from ethereum_test_tools import StateTestFiller, Transaction

Expand Down Expand Up @@ -51,16 +51,27 @@ def callee_bytecode(dest: int, src: int, length: int) -> Bytecode:
return bytecode


@pytest.fixture
def tx_access_list() -> List[AccessList]:
"""
Access list for the transaction.
"""
return [AccessList(address=Address(i), storage_keys=[]) for i in range(1, 10)]


@pytest.fixture
def call_exact_cost(
fork: Fork,
initial_memory: bytes,
dest: int,
length: int,
tx_access_list: List[AccessList],
) -> int:
"""
Returns the exact cost of the subcall, based on the initial memory and the length of the copy.
"""
# Starting from EIP-7623, we need to use an access list to raise the intrinsic gas cost to be
# above the floor data cost.
cost_memory_bytes = fork.memory_expansion_gas_calculator()
gas_costs = fork.gas_costs()
tx_intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator()
Expand All @@ -81,7 +92,7 @@ def call_exact_cost(

sstore_cost = 22100
return (
tx_intrinsic_gas_cost_calculator(calldata=initial_memory)
tx_intrinsic_gas_cost_calculator(calldata=initial_memory, access_list=tx_access_list)
+ mcopy_cost
+ calldatacopy_cost
+ pushes_cost
Expand Down Expand Up @@ -133,10 +144,12 @@ def tx( # noqa: D103
initial_memory: bytes,
tx_max_fee_per_gas: int,
tx_gas_limit: int,
tx_access_list: List[AccessList],
) -> Transaction:
return Transaction(
sender=sender,
to=caller_address,
access_list=tx_access_list,
data=initial_memory,
gas_limit=tx_gas_limit,
max_fee_per_gas=tx_max_fee_per_gas,
Expand Down
4 changes: 4 additions & 0 deletions tests/prague/eip7623_increase_calldata_cost/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""
abstract: Test [EIP-7623: Increase calldata cost](https://eips.ethereum.org/EIPS/eip-7623)
Tests for [EIP-7623: Increase calldata cost](https://eips.ethereum.org/EIPS/eip-7623).
"""
Loading

0 comments on commit 6446ed4

Please sign in to comment.