Skip to content
Closed
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
70 changes: 51 additions & 19 deletions src/ethereum/forks/amsterdam/fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
set_account_balance,
)
from .transactions import (
TX_MAX_GAS_LIMIT,
BlobTransaction,
FeeMarketTransaction,
LegacyTransaction,
Expand Down Expand Up @@ -330,10 +331,12 @@ def execute_block(
block_output.block_access_list
)

if block_output.block_gas_used != block.header.gas_used:
raise InvalidBlock(
f"{block_output.block_gas_used} != {block.header.gas_used}"
)
block_gas_used = max(
block_output.block_gas_used,
block_output.block_state_gas_used,
)
if block_gas_used != block.header.gas_used:
raise InvalidBlock(f"{block_gas_used} != {block.header.gas_used}")
if transactions_root != block.header.transactions_root:
raise InvalidBlock
if block_state_root != block.header.state_root:
Expand Down Expand Up @@ -540,11 +543,16 @@ def check_transaction(
is empty.

"""
gas_available = block_env.block_gas_limit - block_output.block_gas_used
# Regular gas is capped at TX_MAX_GAS_LIMIT per EIP-7825.
# State gas is not checked per-tx; block-end validation enforces
# max(block_regular_gas_used, block_state_gas_used) <= gas_limit.
regular_gas_available = (
block_env.block_gas_limit - block_output.block_gas_used
)
blob_gas_available = MAX_BLOB_GAS_PER_BLOCK - block_output.blob_gas_used

if tx.gas > gas_available:
raise GasUsedExceedsLimitError("gas used exceeds limit")
if min(TX_MAX_GAS_LIMIT, tx.gas) > regular_gas_available:
raise GasUsedExceedsLimitError("regular gas used exceeds limit")

tx_blob_gas_used = calculate_total_blob_gas(tx)
if tx_blob_gas_used > blob_gas_available:
Expand Down Expand Up @@ -763,13 +771,16 @@ def process_unchecked_system_transaction(
origin=SYSTEM_ADDRESS,
gas_price=block_env.base_fee_per_gas,
gas=SYSTEM_TRANSACTION_GAS,
state_gas_reservoir=Uint(0),
access_list_addresses=set(),
access_list_storage_keys=set(),
state=system_tx_state,
blob_versioned_hashes=(),
authorizations=(),
index_in_block=None,
tx_hash=None,
intrinsic_regular_gas=Uint(0),
intrinsic_state_gas=Uint(0),
)

system_tx_message = Message(
Expand All @@ -778,6 +789,7 @@ def process_unchecked_system_transaction(
caller=SYSTEM_ADDRESS,
target=target_address,
gas=SYSTEM_TRANSACTION_GAS,
state_gas_reservoir=Uint(0),
value=U256(0),
data=data,
code=system_contract_code,
Expand Down Expand Up @@ -959,7 +971,9 @@ def process_transaction(
encode_transaction(tx),
)

intrinsic_gas, calldata_floor_gas_cost = validate_transaction(tx)
intrinsic = validate_transaction(tx)

intrinsic_gas = intrinsic.regular + intrinsic.state

(
sender,
Expand All @@ -982,7 +996,12 @@ def process_transaction(

effective_gas_fee = tx.gas * effective_gas_price

gas = tx.gas - intrinsic_gas
# Split execution gas into gas_left (capped by remaining regular gas
# budget) and state_gas_reservoir.
execution_gas = tx.gas - intrinsic_gas
regular_gas_budget = TX_MAX_GAS_LIMIT - intrinsic.regular
gas = min(regular_gas_budget, execution_gas)
state_gas_reservoir = Uint(execution_gas - gas)

increment_nonce(tx_state, sender)

Expand All @@ -1008,13 +1027,16 @@ def process_transaction(
origin=sender,
gas_price=effective_gas_price,
gas=gas,
state_gas_reservoir=state_gas_reservoir,
access_list_addresses=access_list_addresses,
access_list_storage_keys=access_list_storage_keys,
state=tx_state,
blob_versioned_hashes=blob_versioned_hashes,
authorizations=authorizations,
index_in_block=index,
tx_hash=get_transaction_hash(encode_transaction(tx)),
intrinsic_regular_gas=intrinsic.regular,
intrinsic_state_gas=intrinsic.state,
)

message = prepare_message(
Expand All @@ -1025,19 +1047,22 @@ def process_transaction(

tx_output = process_message_call(message)

# For EIP-7623 we first calculate the execution_gas_used, which includes
# the execution gas refund.
tx_gas_used_before_refund = tx.gas - tx_output.gas_left
# With diff-at-return, state_gas_left can exceed its initial value
# (from negative diffs crediting the reservoir). Use saturating
# subtraction to prevent underflow.
total_remaining = tx_output.gas_left + tx_output.state_gas_left
if total_remaining > tx.gas:
tx_gas_used_before_refund = Uint(0)
else:
tx_gas_used_before_refund = tx.gas - total_remaining
tx_gas_refund = min(
tx_gas_used_before_refund // Uint(5), Uint(tx_output.refund_counter)
)
tx_gas_used_after_refund = tx_gas_used_before_refund - tx_gas_refund

# Transactions with less execution_gas_used than the floor pay at the
# floor cost.
tx_gas_used_after_refund = max(
tx_gas_used_after_refund, calldata_floor_gas_cost
)
tx_gas_used = max(tx_gas_used_after_refund, intrinsic.calldata_floor)

tx_gas_left = tx.gas - tx_gas_used_after_refund
gas_refund_amount = tx_gas_left * effective_gas_price
Expand Down Expand Up @@ -1065,11 +1090,17 @@ def process_transaction(
):
destroy_account(tx_state, block_env.coinbase)

block_output.block_gas_used += tx_gas_used_after_refund
tx_regular_gas = tx_env.intrinsic_regular_gas + tx_output.regular_gas_used
tx_state_gas = tx_env.intrinsic_state_gas + tx_output.state_gas_used
block_output.block_gas_used += max(
tx_regular_gas, intrinsic.calldata_floor
)
block_output.block_state_gas_used += tx_state_gas
block_output.blob_gas_used += tx_blob_gas_used

block_output.cumulative_gas_used += tx_gas_used
receipt = make_receipt(
tx, tx_output.error, block_output.block_gas_used, tx_output.logs
tx, tx_output.error, block_output.cumulative_gas_used, tx_output.logs
)

receipt_key = rlp.encode(Uint(index))
Expand All @@ -1083,8 +1114,9 @@ def process_transaction(

block_output.block_logs += tx_output.logs

for address in tx_output.accounts_to_delete:
destroy_account(tx_state, address)
# SELFDESTRUCT same-tx destructions are finalized inside
# process_message at depth 0 so the state_delta_bytes counter
# captures them; no per-tx loop is needed here.

incorporate_tx_into_block(tx_state, block_env.block_access_list_builder)

Expand Down
2 changes: 1 addition & 1 deletion src/ethereum/forks/amsterdam/state_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ def copy_tx_state(tx_state: TransactionState) -> TransactionState:
for addr, slots in tx_state.storage_writes.items()
},
code_writes=dict(tx_state.code_writes),
created_accounts=tx_state.created_accounts,
created_accounts=set(tx_state.created_accounts),
transient_storage=dict(tx_state.transient_storage),
storage_reads=tx_state.storage_reads,
account_reads=tx_state.account_reads,
Expand Down
116 changes: 75 additions & 41 deletions src/ethereum/forks/amsterdam/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,29 @@

from .exceptions import (
InitCodeTooLargeError,
TransactionGasLimitExceededError,
TransactionTypeError,
)
from .fork_types import Authorization, VersionedHash


@dataclass
class IntrinsicGasCost:
"""
Intrinsic gas costs for a transaction, split by gas type.

`regular`: `ethereum.base_types.Uint`
Regular execution gas (calldata, base cost, access list, etc.)
`state`: `ethereum.base_types.Uint`
State growth gas (account creation, storage set, authorization).
`calldata_floor`: `ethereum.base_types.Uint`
Minimum gas cost based on calldata size per [EIP-7623].
"""

regular: Uint
state: Uint
calldata_floor: Uint


TX_MAX_GAS_LIMIT = Uint(16_777_216)


Expand Down Expand Up @@ -513,7 +531,7 @@ def decode_transaction(tx: LegacyTransaction | Bytes) -> Transaction:
return tx


def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]:
def validate_transaction(tx: Transaction) -> IntrinsicGasCost:
"""
Verifies a transaction.

Expand All @@ -531,33 +549,34 @@ def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]:
Also, the code size of a contract creation transaction must be within
limits of the protocol.

This function takes a transaction as a parameter and returns the intrinsic
gas cost and the minimum calldata gas cost for the transaction after
validation. It throws an `InsufficientTransactionGasError` exception if
the transaction does not provide enough gas to cover the intrinsic cost,
and a `NonceOverflowError` exception if the nonce is greater than
`2**64 - 2`. It also raises an `InitCodeTooLargeError` if the code size of
a contract creation transaction exceeds the maximum allowed size.
Returns the intrinsic gas costs for the transaction after validation.
Raises ``InsufficientTransactionGasError`` if the transaction does not
provide enough gas to cover the intrinsic cost, ``NonceOverflowError``
if the nonce overflows, and ``InitCodeTooLargeError`` if a contract
creation transaction's code size exceeds the maximum allowed.

[EIP-2681]: https://eips.ethereum.org/EIPS/eip-2681
[EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623
"""
from .vm.interpreter import MAX_INIT_CODE_SIZE

intrinsic_gas, data_floor_gas_cost = calculate_intrinsic_cost(tx)
if max(intrinsic_gas, data_floor_gas_cost) > tx.gas:
intrinsic = calculate_intrinsic_cost(tx)
intrinsic_gas = intrinsic.regular + intrinsic.state
if max(intrinsic_gas, intrinsic.calldata_floor) > tx.gas:
raise InsufficientTransactionGasError("Insufficient gas")
if max(intrinsic.regular, intrinsic.calldata_floor) > TX_MAX_GAS_LIMIT:
raise InsufficientTransactionGasError(
"Intrinsic regular gas or calldata floor exceeds TX_MAX_GAS_LIMIT"
)
if U256(tx.nonce) >= U256(U64.MAX_VALUE):
raise NonceOverflowError("Nonce too high")
if tx.to == Bytes0(b"") and len(tx.data) > MAX_INIT_CODE_SIZE:
raise InitCodeTooLargeError("Code size too large")
if tx.gas > TX_MAX_GAS_LIMIT:
raise TransactionGasLimitExceededError("Gas limit too high")

return intrinsic_gas, data_floor_gas_cost
return intrinsic


def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]:
def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost:
"""
Calculates the gas that is charged before execution is started.

Expand All @@ -578,37 +597,47 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]:
5. Cost for authorizations (if applicable)


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.
Returns the intrinsic regular gas cost, intrinsic state gas cost, and
the minimum gas cost used by the transaction based on the calldata size.
"""
from .vm.gas import GasCosts, init_code_cost
from .vm.gas import (
COST_PER_STATE_BYTE,
PER_AUTH_BASE_COST,
STATE_BYTES_PER_AUTH_BASE,
STATE_BYTES_PER_NEW_ACCOUNT,
GasCosts,
init_code_cost,
)

tokens_in_calldata = count_tokens_in_data(tx.data)

data_cost = tokens_in_calldata * GasCosts.TX_DATA_TOKEN_STANDARD

create_regular_gas = Uint(0)
create_state_gas = Uint(0)
if tx.to == Bytes0(b""):
create_cost = GasCosts.TX_CREATE + init_code_cost(ulen(tx.data))
else:
create_cost = Uint(0)
create_state_gas = STATE_BYTES_PER_NEW_ACCOUNT * COST_PER_STATE_BYTE
create_regular_gas = (
GasCosts.TX_CREATE + init_code_cost(ulen(tx.data))
)

access_list_cost = Uint(0)
access_list_gas = Uint(0)
tokens_in_access_list = Uint(0)
if has_access_list(tx):
for access in tx.access_list:
access_list_cost += GasCosts.TX_ACCESS_LIST_ADDRESS
access_list_cost += (
access_list_gas += GasCosts.TX_ACCESS_LIST_ADDRESS
access_list_gas += (
ulen(access.slots) * GasCosts.TX_ACCESS_LIST_STORAGE_KEY
)

# Data token floor cost for access list bytes.
access_list_cost += tokens_in_access_list * GasCosts.TX_DATA_TOKEN_FLOOR

auth_cost = Uint(0)
auth_regular_gas = Uint(0)
auth_state_gas = Uint(0)
if isinstance(tx, SetCodeTransaction):
auth_cost += Uint(
GasCosts.AUTH_PER_EMPTY_ACCOUNT * len(tx.authorizations)
auth_regular_gas = PER_AUTH_BASE_COST * ulen(tx.authorizations)
auth_state_gas = (
(STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE)
* COST_PER_STATE_BYTE
* ulen(tx.authorizations)
)

# Floor tokens from calldata.
Expand All @@ -618,19 +647,24 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]:
total_floor_tokens = floor_tokens_in_calldata + tokens_in_access_list

# Floor gas cost (EIP-7623: minimum gas for data-heavy transactions).
data_floor_gas_cost = (
calldata_floor_gas_cost = (
total_floor_tokens * GasCosts.TX_DATA_TOKEN_FLOOR + GasCosts.TX_BASE
)

return (
Uint(
GasCosts.TX_BASE
+ data_cost
+ create_cost
+ access_list_cost
+ auth_cost
),
data_floor_gas_cost,
intrinsic_regular_gas = (
GasCosts.TX_BASE
+ data_cost
+ create_regular_gas
+ access_list_gas
+ auth_regular_gas
)

intrinsic_state_gas = create_state_gas + auth_state_gas

return IntrinsicGasCost(
regular=intrinsic_regular_gas,
state=intrinsic_state_gas,
calldata_floor=calldata_floor_gas_cost,
)


Expand Down
1 change: 1 addition & 0 deletions src/ethereum/forks/amsterdam/utils/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def prepare_message(
caller=tx_env.origin,
target=tx.to,
gas=tx_env.gas,
state_gas_reservoir=tx_env.state_gas_reservoir,
value=tx.value,
data=msg_data,
code=code,
Expand Down
Loading
Loading