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
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
70 changes: 54 additions & 16 deletions src/ethereum/forks/amsterdam/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@
[EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623
"""

CALLDATA_TOKENS_PER_ZERO_BYTE = Uint(1)
"""
Token count contribution of each zero byte in transaction data.
"""

CALLDATA_TOKENS_PER_NONZERO_BYTE = Uint(4)
"""
Token count contribution of each non-zero byte in transaction data.
"""

TX_CREATE_COST = Uint(32000)
"""
Additional gas cost for creating a new contract.
Expand Down Expand Up @@ -556,8 +566,8 @@ def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]:
"""
from .vm.interpreter import MAX_INIT_CODE_SIZE

intrinsic_gas, calldata_floor_gas_cost = calculate_intrinsic_cost(tx)
if max(intrinsic_gas, calldata_floor_gas_cost) > tx.gas:
intrinsic_gas, data_floor_gas_cost = calculate_intrinsic_cost(tx)
if max(intrinsic_gas, data_floor_gas_cost) > tx.gas:
raise InsufficientTransactionGasError("Insufficient gas")
if U256(tx.nonce) >= U256(U64.MAX_VALUE):
raise NonceOverflowError("Nonce too high")
Expand All @@ -566,7 +576,7 @@ def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]:
if tx.gas > TX_MAX_GAS_LIMIT:
raise TransactionGasLimitExceededError("Gas limit too high")

return intrinsic_gas, calldata_floor_gas_cost
return intrinsic_gas, data_floor_gas_cost


def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]:
Expand All @@ -587,26 +597,21 @@ 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 (if applicable)
6. Cost for authorizations (if applicable)

Access lists incur data costs in addition to storage access costs. Token
counting uses `CALLDATA_TOKENS_PER_ZERO_BYTE` and
`CALLDATA_TOKENS_PER_NONZERO_BYTE`.

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 +620,9 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]:
else:
create_cost = Uint(0)

# Calculate access-list tokens and costs.
access_list_cost = Uint(0)
tokens_in_access_list = Uint(0)
if isinstance(
tx,
(
Expand All @@ -626,15 +633,32 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]:
),
):
for access in tx.access_list:
# Storage access costs.
access_list_cost += TX_ACCESS_LIST_ADDRESS_COST
access_list_cost += (
ulen(access.slots) * TX_ACCESS_LIST_STORAGE_KEY_COST
)

# Count data tokens in the 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)
# Always charge data cost for the 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))

# Floor includes all data tokens.
total_data_tokens = tokens_in_calldata + tokens_in_access_list
data_floor_gas_cost = (
total_data_tokens * FLOOR_CALLDATA_COST + TX_BASE_COST
)

return (
Uint(
TX_BASE_COST
Expand All @@ -643,10 +667,24 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]:
+ access_list_cost
+ auth_cost
),
calldata_floor_gas_cost,
data_floor_gas_cost,
)


def count_tokens_in_data(data: bytes) -> Uint:
"""
Count the data tokens in arbitrary input bytes.
"""
tokens = Uint(0)
for byte in data:
if byte == 0:
tokens += CALLDATA_TOKENS_PER_ZERO_BYTE
else:
tokens += CALLDATA_TOKENS_PER_NONZERO_BYTE

return tokens


def recover_sender(chain_id: U64, tx: Transaction) -> Address:
"""
Extracts the sender address from a transaction.
Expand Down
33 changes: 26 additions & 7 deletions src/ethereum/forks/arrow_glacier/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@
Gas cost per zero byte in the transaction data.
"""

CALLDATA_TOKENS_PER_ZERO_BYTE = Uint(1)
"""
Token count contribution of each zero byte in transaction data.
"""

CALLDATA_TOKENS_PER_NONZERO_BYTE = Uint(4)
"""
Token count contribution of each non-zero byte in transaction data.
"""

TX_CREATE_COST = Uint(32000)
"""
Additional gas cost for creating a new contract.
Expand Down Expand Up @@ -378,13 +388,8 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint:
This function takes a transaction as a parameter and returns the intrinsic
gas cost of the transaction.
"""
data_cost = Uint(0)

for byte in tx.data:
if byte == 0:
data_cost += TX_DATA_COST_PER_ZERO
else:
data_cost += TX_DATA_COST_PER_NON_ZERO
tokens_in_calldata = count_tokens_in_data(tx.data)
data_cost = tokens_in_calldata * TX_DATA_COST_PER_ZERO

if tx.to == Bytes0(b""):
create_cost = TX_CREATE_COST
Expand All @@ -402,6 +407,20 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint:
return TX_BASE_COST + data_cost + create_cost + access_list_cost


def count_tokens_in_data(data: bytes) -> Uint:
"""
Count the data tokens in arbitrary input bytes.
"""
tokens = Uint(0)
for byte in data:
if byte == 0:
tokens += CALLDATA_TOKENS_PER_ZERO_BYTE
else:
tokens += CALLDATA_TOKENS_PER_NONZERO_BYTE

return tokens


def recover_sender(chain_id: U64, tx: Transaction) -> Address:
"""
Extracts the sender address from a transaction.
Expand Down
Loading
Loading