Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 6 additions & 1 deletion packages/testing/src/execution_testing/forks/base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ class TransactionDataFloorCostCalculator(Protocol):
Calculate the transaction floor cost due to its calldata for a given fork.
"""

def __call__(self, *, data: BytesConvertible) -> int:
def __call__(
self,
*,
data: BytesConvertible,
access_list: List[AccessList] | None = None,
) -> int:
"""Return transaction gas cost of calldata given its contents."""
pass

Expand Down
119 changes: 116 additions & 3 deletions packages/testing/src/execution_testing/forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,8 +765,12 @@ def transaction_data_floor_cost_calculator(
"""At frontier, the transaction data floor cost is a constant zero."""
del block_number, timestamp

def fn(*, data: BytesConvertible) -> int:
del data
def fn(
*,
data: BytesConvertible,
access_list: List[AccessList] | None = None,
) -> int:
del data, access_list
return 0

return fn
Expand Down Expand Up @@ -2800,7 +2804,12 @@ def transaction_data_floor_cost_calculator(
block_number=block_number, timestamp=timestamp
)

def fn(*, data: BytesConvertible) -> int:
def fn(
*,
data: BytesConvertible,
access_list: List[AccessList] | None = None,
) -> int:
del access_list
return (
calldata_gas_calculator(data=data, floor=True)
+ gas_costs.G_TRANSACTION
Expand Down Expand Up @@ -3349,6 +3358,110 @@ def is_deployed(cls) -> bool:
"""Return True if this fork is deployed."""
return False

@classmethod
def _access_list_token_count(
cls, access_list: List[AccessList] | None
) -> int:
"""
Return the total number of data tokens contributed by an access list.

Tokens are counted per EIP-7981:
- zero byte = 1 token
- non-zero byte = 4 tokens
"""
if not access_list:
return 0

tokens = 0
for access in access_list:
for b in access.address:
tokens += 1 if b == 0 else 4
for slot in access.storage_keys:
for b in slot:
tokens += 1 if b == 0 else 4
return tokens

@classmethod
def transaction_data_floor_cost_calculator(
cls, *, block_number: int = 0, timestamp: int = 0
) -> TransactionDataFloorCostCalculator:
"""
On Amsterdam, the floor cost includes calldata and access list tokens
per EIP-7981.
"""
calldata_gas_calculator = cls.calldata_gas_calculator(
block_number=block_number, timestamp=timestamp
)
gas_costs = cls.gas_costs(
block_number=block_number, timestamp=timestamp
)

def fn(
*,
data: BytesConvertible,
access_list: List[AccessList] | None = None,
) -> int:
access_list_tokens = cls._access_list_token_count(access_list)
return (
calldata_gas_calculator(data=data, floor=True)
+ access_list_tokens * gas_costs.G_TX_DATA_FLOOR_TOKEN_COST
+ gas_costs.G_TRANSACTION
)

return fn

@classmethod
def transaction_intrinsic_cost_calculator(
cls, *, block_number: int = 0, timestamp: int = 0
) -> TransactionIntrinsicCostCalculator:
"""
On Amsterdam, access list data is charged at the floor token cost and
contributes to the floor gas cost per EIP-7981.
"""
super_fn = super(Amsterdam, cls).transaction_intrinsic_cost_calculator(
block_number=block_number, timestamp=timestamp
)
gas_costs = cls.gas_costs(
block_number=block_number, timestamp=timestamp
)
transaction_data_floor_cost_calculator = (
cls.transaction_data_floor_cost_calculator(
block_number=block_number, timestamp=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,
authorization_list_or_count=authorization_list_or_count,
return_cost_deducted_prior_execution=True,
)
access_list_tokens = cls._access_list_token_count(access_list)
intrinsic_cost += (
access_list_tokens * gas_costs.G_TX_DATA_FLOOR_TOKEN_COST
)

if return_cost_deducted_prior_execution:
return intrinsic_cost

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

return fn

@classmethod
def engine_new_payload_version(
cls, *, block_number: int = 0, timestamp: int = 0
Expand Down
51 changes: 39 additions & 12 deletions src/ethereum/forks/amsterdam/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,18 @@ def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]:
return intrinsic_gas, calldata_floor_gas_cost


def count_tokens_in_data(data: bytes) -> Uint:
"""
Count the tokens in an arbitrary input data.
"""
zero_bytes = 0
for byte in data:
if byte == 0:
zero_bytes += 1
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the 1 be in a named constant? Something like CALLDATA_TOKENS_PER_ZERO_BYTE?


return Uint(zero_bytes + (len(data) - zero_bytes) * 4)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be something like:

Suggested change
return Uint(zero_bytes + (len(data) - zero_bytes) * 4)
return Uint(zero_bytes + (len(data) - zero_bytes) * CALLDATA_TOKENS_PER_NONZERO_BYTE)

?

Copy link
Contributor

Choose a reason for hiding this comment

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

If we're iterating through all of data anyway, how about:

Suggested change
zero_bytes = 0
for byte in data:
if byte == 0:
zero_bytes += 1
return Uint(zero_bytes + (len(data) - zero_bytes) * 4)
return sum(Uint(4) if byte else Uint(1) for byte in data)

Or if that's too Pythonic:

Suggested change
zero_bytes = 0
for byte in data:
if byte == 0:
zero_bytes += 1
return Uint(zero_bytes + (len(data) - zero_bytes) * 4)
return sum(Uint(1) if byte == 0 else Uint(4) for byte in data)



Copy link
Contributor

Choose a reason for hiding this comment

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

This should be backported to previous forks.

def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]:
"""
Calculates the gas that is charged before execution is started.
Expand All @@ -587,26 +599,22 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]:
2. Cost for data (zero and non-zero bytes)
3. Cost for contract creation (if applicable)
4. Cost for access list entries (if applicable)
5. Cost for authorizations (if applicable)
5. Cost for access list data (EIP-7981)
6. Cost for authorizations (if applicable)

Per EIP-7981, access lists now incur data costs in addition to storage
access costs. The data cost is calculated based on the number of bytes
in the access list (addresses and storage keys), with zero bytes costing
1 token and non-zero bytes costing 4 tokens.
Copy link
Contributor

Choose a reason for hiding this comment

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

We should avoid putting constant values in docstrings. It's too easy to update the code and forget to update the docs. Instead, link to the relevant constant.


This function takes a transaction as a parameter and returns the intrinsic
gas cost of the transaction and the minimum gas cost used by the
transaction based on the calldata size.
transaction based on the calldata and access list size.
"""
from .vm.eoa_delegation import PER_EMPTY_ACCOUNT_COST
from .vm.gas import init_code_cost

zero_bytes = 0
for byte in tx.data:
if byte == 0:
zero_bytes += 1

tokens_in_calldata = Uint(zero_bytes + (len(tx.data) - zero_bytes) * 4)
# EIP-7623 floor price (note: no EVM costs)
calldata_floor_gas_cost = (
tokens_in_calldata * FLOOR_CALLDATA_COST + TX_BASE_COST
)
tokens_in_calldata = count_tokens_in_data(tx.data)

data_cost = tokens_in_calldata * STANDARD_CALLDATA_TOKEN_COST

Expand All @@ -615,7 +623,9 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]:
else:
create_cost = Uint(0)

# EIP-7981: Calculate access list tokens and costs
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure we should establish the pattern of commenting with an EIP number. If we take this approach, we'll end up with piles of comments where a block of code is introduced under EIP-X, then in a later fork EIP-Y will modify that block, making it misleading which EIP a sequence of lines belongs to. For example:

Fork T Fork T+1
# EIP-1234: The dingus is the rate of fleep      
dingus = a + b
dingus += c ^ d
dingus /= fleep(e)
# EIP-1234: The dingus is the rate of fleep      
dingus = a + b

# EIP-4567: Frobulate the dingus
dingus = frobulate(dingus)

dingus += c ^ d        # <-
dingus /= fleep(e)     # <-

The marked lines (<-) are now incorrectly attributed to EIP-4567 in Fork+1. Instead, I'd recommend omitting the EIP identifier in the comments, and instead describe the changes introduced by the EIP in the function's docstrings. The rendered diffs will make it pretty obvious what's changed.

Perhaps I'm being overly cautious, however.

access_list_cost = Uint(0)
tokens_in_access_list = Uint(0)
if isinstance(
tx,
(
Expand All @@ -626,15 +636,32 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]:
),
):
for access in tx.access_list:
# Storage access costs (EIP-2930)
Copy link
Contributor

Choose a reason for hiding this comment

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

You'll need to backport the comment as well.

access_list_cost += TX_ACCESS_LIST_ADDRESS_COST
access_list_cost += (
ulen(access.slots) * TX_ACCESS_LIST_STORAGE_KEY_COST
)

# EIP-7981: Count data tokens in access list
access_list_data = b""
for access in tx.access_list:
access_list_data += bytes(access.account)
for slot in access.slots:
access_list_data += bytes(slot)
tokens_in_access_list = count_tokens_in_data(access_list_data)
# EIP-7981: Always charge data cost for access list
access_list_cost += tokens_in_access_list * FLOOR_CALLDATA_COST

auth_cost = Uint(0)
if isinstance(tx, SetCodeTransaction):
auth_cost += Uint(PER_EMPTY_ACCOUNT_COST * len(tx.authorizations))

# EIP-7623/7981: Floor includes all data tokens
total_data_tokens = tokens_in_calldata + tokens_in_access_list
calldata_floor_gas_cost = (
total_data_tokens * FLOOR_CALLDATA_COST + TX_BASE_COST
)

return (
Uint(
TX_BASE_COST
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for EIP-7981: Increase Access List Cost."""
Loading
Loading