From 65d6a147a50644774fc3949f5727424d8e67a36d Mon Sep 17 00:00:00 2001 From: lightclient Date: Mon, 6 Jul 2026 05:48:31 -0600 Subject: [PATCH 1/9] fork(bogota): add bogota fork --- src/ethereum/forks/bogota/__init__.py | 17 + .../forks/bogota/block_access_lists.py | 740 ++++++++++ src/ethereum/forks/bogota/blocks.py | 435 ++++++ src/ethereum/forks/bogota/bloom.py | 87 ++ src/ethereum/forks/bogota/exceptions.py | 155 +++ src/ethereum/forks/bogota/fork.py | 1238 +++++++++++++++++ src/ethereum/forks/bogota/fork_types.py | 97 ++ src/ethereum/forks/bogota/requests.py | 326 +++++ src/ethereum/forks/bogota/state_tracker.py | 871 ++++++++++++ src/ethereum/forks/bogota/transactions.py | 1031 ++++++++++++++ src/ethereum/forks/bogota/utils/__init__.py | 3 + src/ethereum/forks/bogota/utils/address.py | 93 ++ .../forks/bogota/utils/hexadecimal.py | 54 + src/ethereum/forks/bogota/utils/message.py | 94 ++ src/ethereum/forks/bogota/vm/__init__.py | 342 +++++ .../forks/bogota/vm/eoa_delegation.py | 282 ++++ src/ethereum/forks/bogota/vm/exceptions.py | 139 ++ src/ethereum/forks/bogota/vm/gas.py | 596 ++++++++ .../forks/bogota/vm/instructions/__init__.py | 377 +++++ .../bogota/vm/instructions/arithmetic.py | 371 +++++ .../forks/bogota/vm/instructions/bitwise.py | 277 ++++ .../forks/bogota/vm/instructions/block.py | 294 ++++ .../bogota/vm/instructions/comparison.py | 180 +++ .../bogota/vm/instructions/control_flow.py | 174 +++ .../bogota/vm/instructions/environment.py | 611 ++++++++ .../forks/bogota/vm/instructions/keccak.py | 65 + .../forks/bogota/vm/instructions/log.py | 87 ++ .../forks/bogota/vm/instructions/memory.py | 178 +++ .../forks/bogota/vm/instructions/stack.py | 317 +++++ .../forks/bogota/vm/instructions/storage.py | 196 +++ .../forks/bogota/vm/instructions/system.py | 933 +++++++++++++ src/ethereum/forks/bogota/vm/interpreter.py | 375 +++++ src/ethereum/forks/bogota/vm/memory.py | 83 ++ .../vm/precompiled_contracts/__init__.py | 55 + .../vm/precompiled_contracts/alt_bn128.py | 234 ++++ .../vm/precompiled_contracts/blake2f.py | 42 + .../bls12_381/__init__.py | 622 +++++++++ .../bls12_381/bls12_381_g1.py | 149 ++ .../bls12_381/bls12_381_g2.py | 151 ++ .../bls12_381/bls12_381_pairing.py | 69 + .../vm/precompiled_contracts/ecrecover.py | 64 + .../vm/precompiled_contracts/identity.py | 46 + .../vm/precompiled_contracts/mapping.py | 78 ++ .../bogota/vm/precompiled_contracts/modexp.py | 175 +++ .../vm/precompiled_contracts/p256verify.py | 90 ++ .../precompiled_contracts/point_evaluation.py | 72 + .../vm/precompiled_contracts/ripemd160.py | 51 + .../bogota/vm/precompiled_contracts/sha256.py | 48 + src/ethereum/forks/bogota/vm/runtime.py | 95 ++ src/ethereum/forks/bogota/vm/stack.py | 131 ++ 50 files changed, 13290 insertions(+) create mode 100644 src/ethereum/forks/bogota/__init__.py create mode 100644 src/ethereum/forks/bogota/block_access_lists.py create mode 100644 src/ethereum/forks/bogota/blocks.py create mode 100644 src/ethereum/forks/bogota/bloom.py create mode 100644 src/ethereum/forks/bogota/exceptions.py create mode 100644 src/ethereum/forks/bogota/fork.py create mode 100644 src/ethereum/forks/bogota/fork_types.py create mode 100644 src/ethereum/forks/bogota/requests.py create mode 100644 src/ethereum/forks/bogota/state_tracker.py create mode 100644 src/ethereum/forks/bogota/transactions.py create mode 100644 src/ethereum/forks/bogota/utils/__init__.py create mode 100644 src/ethereum/forks/bogota/utils/address.py create mode 100644 src/ethereum/forks/bogota/utils/hexadecimal.py create mode 100644 src/ethereum/forks/bogota/utils/message.py create mode 100644 src/ethereum/forks/bogota/vm/__init__.py create mode 100644 src/ethereum/forks/bogota/vm/eoa_delegation.py create mode 100644 src/ethereum/forks/bogota/vm/exceptions.py create mode 100644 src/ethereum/forks/bogota/vm/gas.py create mode 100644 src/ethereum/forks/bogota/vm/instructions/__init__.py create mode 100644 src/ethereum/forks/bogota/vm/instructions/arithmetic.py create mode 100644 src/ethereum/forks/bogota/vm/instructions/bitwise.py create mode 100644 src/ethereum/forks/bogota/vm/instructions/block.py create mode 100644 src/ethereum/forks/bogota/vm/instructions/comparison.py create mode 100644 src/ethereum/forks/bogota/vm/instructions/control_flow.py create mode 100644 src/ethereum/forks/bogota/vm/instructions/environment.py create mode 100644 src/ethereum/forks/bogota/vm/instructions/keccak.py create mode 100644 src/ethereum/forks/bogota/vm/instructions/log.py create mode 100644 src/ethereum/forks/bogota/vm/instructions/memory.py create mode 100644 src/ethereum/forks/bogota/vm/instructions/stack.py create mode 100644 src/ethereum/forks/bogota/vm/instructions/storage.py create mode 100644 src/ethereum/forks/bogota/vm/instructions/system.py create mode 100644 src/ethereum/forks/bogota/vm/interpreter.py create mode 100644 src/ethereum/forks/bogota/vm/memory.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/__init__.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/alt_bn128.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/blake2f.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/__init__.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/bls12_381_g1.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/bls12_381_g2.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/ecrecover.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/identity.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/mapping.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/modexp.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/p256verify.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/point_evaluation.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/ripemd160.py create mode 100644 src/ethereum/forks/bogota/vm/precompiled_contracts/sha256.py create mode 100644 src/ethereum/forks/bogota/vm/runtime.py create mode 100644 src/ethereum/forks/bogota/vm/stack.py diff --git a/src/ethereum/forks/bogota/__init__.py b/src/ethereum/forks/bogota/__init__.py new file mode 100644 index 00000000000..65069fa3ccd --- /dev/null +++ b/src/ethereum/forks/bogota/__init__.py @@ -0,0 +1,17 @@ +""" +The Bogota fork includes the frame transaction, which decomposes a +transaction into a sequence of frames that validate the transaction, +approve gas payment, and execute user operations. + +### Changes + +- [EIP-8141: Frame Transaction][EIP-8141] + +### Releases + +[EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 +""" + +from ethereum.fork_criteria import ForkCriteria, Unscheduled + +FORK_CRITERIA: ForkCriteria = Unscheduled(order_index=4) diff --git a/src/ethereum/forks/bogota/block_access_lists.py b/src/ethereum/forks/bogota/block_access_lists.py new file mode 100644 index 00000000000..34110f13bf7 --- /dev/null +++ b/src/ethereum/forks/bogota/block_access_lists.py @@ -0,0 +1,740 @@ +""" +Block access lists (BALs), originally defined in [EIP-7928], record all +accounts and storage locations accessed during block execution along with their +post-execution values. + +BALs enable parallel disk reads, parallel transaction validation, parallel +state root computation, and applying state updates without executing bytecode. + +See [`BlockAccessList`][bal] for more detail. + +[EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 +[bal]: ref:ethereum.forks.bogota.block_access_lists.BlockAccessList +""" + +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Set, Tuple, TypeAlias, final + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U64, U256, Uint, ulen + +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.state import EMPTY_CODE_HASH, Account, Address, PreState + +from .exceptions import BlockAccessListGasLimitExceededError +from .fork_types import BlockAccessIndex +from .state_tracker import BlockState, TransactionState, get_code + + +@final +@slotted_freezable +@dataclass +class StorageChange: + """ + In a [`SlotChanges`][s], represents a single change in an [`Account`]'s + storage slot. + + [s]: ref:ethereum.forks.bogota.block_access_lists.SlotChanges + [`Account`]: ref:ethereum.state.Account + """ + + block_access_index: BlockAccessIndex + """ + Position within the set of all changes in a [`Block`]. + + [`Block`]: ref:ethereum.forks.bogota.blocks.Block + """ + + new_value: U256 + """ + Value of an [`Account`]'s storage slot after this change has been applied. + + [`Account`]: ref:ethereum.state.Account + """ + + +@final +@slotted_freezable +@dataclass +class BalanceChange: + """ + In a [`BlockAccessList`][bal], represents a change in an [`Account`]'s + balance. + + [bal]: ref:ethereum.forks.bogota.block_access_lists.BlockAccessList + [`Account`]: ref:ethereum.state.Account + """ # noqa: E501 + + block_access_index: BlockAccessIndex + """ + Position within the set of all changes in a [`Block`]. + + [`Block`]: ref:ethereum.forks.bogota.blocks.Block + """ + + post_balance: U256 + """ + Balance of an [`Account`] after this change has been applied. + + [`Account`]: ref:ethereum.state.Account + """ + + +@final +@slotted_freezable +@dataclass +class NonceChange: + """ + In a [`BlockAccessList`][bal], represents a change in an [`Account`]'s + nonce. + + [bal]: ref:ethereum.forks.bogota.block_access_lists.BlockAccessList + [`Account`]: ref:ethereum.state.Account + """ # noqa: E501 + + block_access_index: BlockAccessIndex + """ + Position within the set of all changes in a [`Block`]. + + [`Block`]: ref:ethereum.forks.bogota.blocks.Block + """ + + new_nonce: U64 + """ + Nonce of an [`Account`] after this change has been applied. + + [`Account`]: ref:ethereum.state.Account + """ + + +@final +@slotted_freezable +@dataclass +class CodeChange: + """ + In a [`BlockAccessList`][bal], represents a change in an [`Account`]'s + code. + + [bal]: ref:ethereum.forks.bogota.block_access_lists.BlockAccessList + [`Account`]: ref:ethereum.state.Account + """ # noqa: E501 + + block_access_index: BlockAccessIndex + """ + Position within the set of all changes in a [`Block`]. + + [`Block`]: ref:ethereum.forks.bogota.blocks.Block + """ + + new_code: Bytes + """ + Code of an [`Account`] after this change has been applied. + + [`Account`]: ref:ethereum.state.Account + """ + + +@final +@slotted_freezable +@dataclass +class SlotChanges: + """ + In a [`BlockAccessList`][bal], represents a change in an [`Account`]'s + storage. + + [bal]: ref:ethereum.forks.bogota.block_access_lists.BlockAccessList + [`Account`]: ref:ethereum.state.Account + """ # noqa: E501 + + slot: U256 + """ + Location within an [`Account`]'s storage that has been modified. + + [`Account`]: ref:ethereum.state.Account + """ + + changes: Tuple[StorageChange, ...] + """ + Sequence of changes that have been made to one particular storage slot. + """ + + +@final +@slotted_freezable +@dataclass +class AccountChanges: + """ + All changes for a single [`Account`], grouped by field type. + + [`Account`]: ref:ethereum.state.Account + """ + + address: Address + """ + Address of the account containing these changes. + """ + + storage_changes: Tuple[SlotChanges, ...] + """ + Writes to the storage of the associated [`Account`]. + + [`Account`]: ref:ethereum.state.Account + """ + + storage_reads: Tuple[U256, ...] + """ + Storage slots of the associated [`Account`] that have been read but not + changed. + + [`Account`]: ref:ethereum.state.Account + """ + + balance_changes: Tuple[BalanceChange, ...] + """ + Writes to the balance of the associated [`Account`]. + + [`Account`]: ref:ethereum.state.Account + """ + + nonce_changes: Tuple[NonceChange, ...] + """ + Writes to the nonce of the associated [`Account`]. + + [`Account`]: ref:ethereum.state.Account + """ + + code_changes: Tuple[CodeChange, ...] + """ + Writes to the code of the associated [`Account`]. + + [`Account`]: ref:ethereum.state.Account + """ + + +BlockAccessList: TypeAlias = List[AccountChanges] +""" +List of state changes recorded across a [`Block`]. + +The hash of a block's access list is included in its [`Header`], though the +access list itself is not included in the block body. + +A `BlockAccessList` includes, for example, the targets of: + +- [`BALANCE`], [`EXTCODESIZE`][ecs], [`EXTCODECOPY`][ecc], + and [`EXTCODEHASH`][ech] instructions; +- the [call family][call] of instructions _even if they revert_; +- the [create family][create] of instructions if the target is accessed; +- etc. + +[`Block`]: ref:ethereum.forks.bogota.blocks.Block +[`Header`]: ref:ethereum.forks.bogota.blocks.Header +[`BALANCE`]: ref:ethereum.forks.bogota.vm.instructions.environment.balance +[ecs]: ref:ethereum.forks.bogota.vm.instructions.environment.extcodesize +[ecc]: ref:ethereum.forks.bogota.vm.instructions.environment.extcodecopy +[ech]: ref:ethereum.forks.bogota.vm.instructions.environment.extcodehash +[call]: ref:ethereum.forks.bogota.vm.instructions.system.call +[create]: ref:ethereum.forks.bogota.vm.instructions.system.create +""" + + +@final +@dataclass +class AccountData: + """ + Account data stored in the builder during block execution. + + This dataclass tracks all changes made to a single account throughout + the execution of a block, organized by the type of change and the + transaction index where it occurred. + """ + + storage_changes: Dict[U256, List[StorageChange]] = field( + default_factory=dict + ) + """ + Mapping from storage slot to list of changes made to that slot. + Each change includes the transaction index and new value. + """ + + storage_reads: Set[U256] = field(default_factory=set) + """ + Set of storage slots that were read but not modified. + """ + + balance_changes: List[BalanceChange] = field(default_factory=list) + """ + List of balance changes for this account, ordered by transaction index. + """ + + nonce_changes: List[NonceChange] = field(default_factory=list) + """ + List of nonce changes for this account, ordered by transaction index. + """ + + code_changes: List[CodeChange] = field(default_factory=list) + """ + List of code changes (contract deployments) for this account, + ordered by transaction index. + """ + + +@final +@dataclass +class BlockAccessListBuilder: + """ + Builder for constructing [`BlockAccessList`] efficiently during transaction + execution. + + The builder accumulates all account and storage accesses during block + execution and constructs a deterministic access list. Changes are tracked + by address, field type, and transaction index to enable efficient + reconstruction of state changes. + + The builder follows a two-phase approach: + + 1. **Collection Phase**: During transaction execution, all state accesses + are recorded via the tracking functions. + 1. **Build Phase**: After block execution, the accumulated data is sorted + and encoded into the final deterministic format. + + [`BlockAccessList`]: ref:ethereum.forks.bogota.block_access_lists.BlockAccessList + """ # noqa: E501 + + block_access_index: BlockAccessIndex = BlockAccessIndex(0) + """ + Current block access index. Set by the caller before each + [`incorporate_tx_into_block`] call (0 for system txs, i+1 for the + i-th user tx, N+1 for post-execution operations). + + [`incorporate_tx_into_block`]: ref:ethereum.forks.bogota.state_tracker.incorporate_tx_into_block + """ # noqa: E501 + + accounts: Dict[Address, AccountData] = field(default_factory=dict) + """ + Mapping from account address to its tracked changes during block execution. + """ + + +def ensure_account(builder: BlockAccessListBuilder, address: Address) -> None: + """ + Ensure an account exists in the builder's tracking structure. + + Creates an empty [`AccountData`][ad] entry for the given address if it + doesn't already exist. This function is idempotent and safe to call + multiple times for the same address. + + [ad]: ref:ethereum.forks.bogota.block_access_lists.AccountData + """ + if address not in builder.accounts: + builder.accounts[address] = AccountData() + + +def add_storage_write( + builder: BlockAccessListBuilder, + address: Address, + slot: U256, + block_access_index: BlockAccessIndex, + new_value: U256, +) -> None: + """ + Add a storage write operation to the block access list. + + Records a storage slot modification for a given address at a specific + transaction index. If multiple writes occur to the same slot within the + same transaction (same `block_access_index`), only the final value is kept. + """ + ensure_account(builder, address) + + if slot not in builder.accounts[address].storage_changes: + builder.accounts[address].storage_changes[slot] = [] + + # Check if there's already an entry with the same block_access_index + # If so, update it with the new value, keeping only the final write + changes = builder.accounts[address].storage_changes[slot] + for i, existing_change in enumerate(changes): + if existing_change.block_access_index == block_access_index: + # Update the existing entry with the new value + changes[i] = StorageChange( + block_access_index=block_access_index, new_value=new_value + ) + return + + # No existing entry found, append new change + change = StorageChange( + block_access_index=block_access_index, new_value=new_value + ) + builder.accounts[address].storage_changes[slot].append(change) + + +def add_storage_read( + builder: BlockAccessListBuilder, address: Address, slot: U256 +) -> None: + """ + Add a storage read operation to the block access list. + + Records that a storage slot was read during execution. Storage slots + that are both read and written will only appear in the storage changes + list, not in the storage reads list, as per [EIP-7928]. + """ + ensure_account(builder, address) + builder.accounts[address].storage_reads.add(slot) + + +def add_balance_change( + builder: BlockAccessListBuilder, + address: Address, + block_access_index: BlockAccessIndex, + post_balance: U256, +) -> None: + """ + Add a balance change to the block access list. + + Records the post-transaction balance for an account after it has been + modified. This includes changes from transfers, gas fees, block rewards, + and any other balance-affecting operations. + """ + ensure_account(builder, address) + + # Balance value is already U256 + balance_value = post_balance + + # Check if we already have a balance change for this tx_index and update it + # This ensures we only track the final balance per transaction + existing_changes = builder.accounts[address].balance_changes + for i, existing in enumerate(existing_changes): + if existing.block_access_index == block_access_index: + # Update the existing balance change with the new balance + existing_changes[i] = BalanceChange( + block_access_index=block_access_index, + post_balance=balance_value, + ) + return + + # No existing change for this tx_index, add a new one + change = BalanceChange( + block_access_index=block_access_index, post_balance=balance_value + ) + builder.accounts[address].balance_changes.append(change) + + +def add_nonce_change( + builder: BlockAccessListBuilder, + address: Address, + block_access_index: BlockAccessIndex, + new_nonce: U64, +) -> None: + """ + Add a nonce change to the block access list. + + Records a nonce increment for an account. This occurs when an EOA sends + a transaction or when a contract performs [`CREATE`] or [`CREATE2`] + operations. + + [`CREATE`]: ref:ethereum.forks.bogota.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.forks.bogota.vm.instructions.system.create2 + """ + ensure_account(builder, address) + + # Check if we already have a nonce change for this tx_index and update it + # This ensures we only track the final (highest) nonce per transaction + existing_changes = builder.accounts[address].nonce_changes + for i, existing in enumerate(existing_changes): + if existing.block_access_index == block_access_index: + # Keep the highest nonce value + if new_nonce > existing.new_nonce: + existing_changes[i] = NonceChange( + block_access_index=block_access_index, new_nonce=new_nonce + ) + return + + # No existing change for this tx_index, add a new one + change = NonceChange( + block_access_index=block_access_index, new_nonce=new_nonce + ) + builder.accounts[address].nonce_changes.append(change) + + +def add_code_change( + builder: BlockAccessListBuilder, + address: Address, + block_access_index: BlockAccessIndex, + new_code: Bytes, +) -> None: + """ + Add a code change to the block access list. + + Records contract code deployment or modification. This typically occurs + during contract creation via [`CREATE`], [`CREATE2`], or + [`SetCodeTransaction`][sct] operations. + + [`CREATE`]: ref:ethereum.forks.bogota.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.forks.bogota.vm.instructions.system.create2 + [sct]: ref:ethereum.forks.bogota.transactions.SetCodeTransaction + """ + ensure_account(builder, address) + + # Check if we already have a code change for this block_access_index + # This handles the case of in-transaction selfdestructs where code is + # first deployed and then cleared in the same transaction + existing_changes = builder.accounts[address].code_changes + for i, existing in enumerate(existing_changes): + if existing.block_access_index == block_access_index: + # Replace the existing code change with the new one + # For selfdestructs, this ensures we only record the final + # state (empty code) + existing_changes[i] = CodeChange( + block_access_index=block_access_index, new_code=new_code + ) + return + + # No existing change for this block_access_index, add a new one + change = CodeChange( + block_access_index=block_access_index, new_code=new_code + ) + builder.accounts[address].code_changes.append(change) + + +def add_touched_account( + builder: BlockAccessListBuilder, address: Address +) -> None: + """ + Add an account that was accessed but not modified. + + Records that an account was accessed during execution without any state + changes. This is used for operations like [`EXTCODEHASH`], [`BALANCE`], + [`EXTCODESIZE`], and [`EXTCODECOPY`] that read account data without + modifying it. + + [`EXTCODEHASH`]: ref:ethereum.forks.bogota.vm.instructions.environment.extcodehash + [`BALANCE`]: ref:ethereum.forks.bogota.vm.instructions.environment.balance + [`EXTCODESIZE`]: ref:ethereum.forks.bogota.vm.instructions.environment.extcodesize + [`EXTCODECOPY`]: ref:ethereum.forks.bogota.vm.instructions.environment.extcodecopy + """ # noqa: E501 + ensure_account(builder, address) + + +def _build_from_builder( + builder: BlockAccessListBuilder, +) -> BlockAccessList: + """ + Build the final [`BlockAccessList`] from a builder (internal helper). + + Constructs a deterministic block access list by sorting all accumulated + changes. The resulting list is ordered by: + + 1. Account addresses (lexicographically) + 2. Within each account: + - Storage slots (lexicographically) + - Transaction indices (numerically) for each change type + + Addresses, storage slots, and block access indices are unique. + Storage reads that also appear in storage changes are excluded. + + [`BlockAccessList`]: ref:ethereum.forks.bogota.block_access_lists.BlockAccessList + """ # noqa: E501 + block_access_list: BlockAccessList = [] + + for address, changes in builder.accounts.items(): + storage_changes = [] + for slot, slot_changes in changes.storage_changes.items(): + sorted_changes = tuple( + sorted(slot_changes, key=lambda x: x.block_access_index) + ) + storage_changes.append( + SlotChanges(slot=slot, changes=sorted_changes) + ) + + storage_reads = [] + for slot in changes.storage_reads: + if slot not in changes.storage_changes: + storage_reads.append(slot) + + balance_changes = tuple( + sorted(changes.balance_changes, key=lambda x: x.block_access_index) + ) + nonce_changes = tuple( + sorted(changes.nonce_changes, key=lambda x: x.block_access_index) + ) + code_changes = tuple( + sorted(changes.code_changes, key=lambda x: x.block_access_index) + ) + + storage_changes.sort(key=lambda x: x.slot) + storage_reads.sort() + + account_change = AccountChanges( + address=address, + storage_changes=tuple(storage_changes), + storage_reads=tuple(storage_reads), + balance_changes=balance_changes, + nonce_changes=nonce_changes, + code_changes=code_changes, + ) + + block_access_list.append(account_change) + + block_access_list.sort(key=lambda x: x.address) + + return block_access_list + + +def _get_pre_tx_account( + pre_tx_accounts: Dict[Address, Optional[Account]], + pre_state: PreState, + address: Address, +) -> Optional[Account]: + """ + Look up an account in cumulative state, falling back to `pre_state`. + + The cumulative account state (`pre_tx_accounts`) should contain state up + to (but not including) the current transaction. + + Returns `None` if the `address` does not exist. + """ + if address in pre_tx_accounts: + return pre_tx_accounts[address] + return pre_state.get_account_optional(address) + + +def _get_pre_tx_storage( + pre_tx_storage: Dict[Address, Dict[Bytes32, U256]], + pre_state: PreState, + address: Address, + key: Bytes32, +) -> U256: + """ + Look up a storage value in cumulative state, falling back to `pre_state`. + + Returns `0` if not set. + """ + if address in pre_tx_storage and key in pre_tx_storage[address]: + return pre_tx_storage[address][key] + return pre_state.get_storage(address, key) + + +def update_builder_from_tx( + builder: BlockAccessListBuilder, + tx_state: TransactionState, +) -> None: + """ + Update the BAL builder with changes from a single transaction. + + Compare the transaction's writes against the block's cumulative + state (falling back to `pre_state`) to extract balance, nonce, code, and + storage changes. Net-zero filtering is automatic: if the pre-tx value + equals the post-tx value, no change is recorded. + + Must be called **before** the transaction's writes are merged into + the block state. + """ + block_state = tx_state.parent + pre_state = block_state.pre_state + idx = builder.block_access_index + + # Compare account writes against block cumulative state + for address, post_account in tx_state.account_writes.items(): + pre_account = _get_pre_tx_account( + block_state.account_writes, pre_state, address + ) + + pre_balance = pre_account.balance if pre_account else U256(0) + post_balance = post_account.balance if post_account else U256(0) + if pre_balance != post_balance: + add_balance_change(builder, address, idx, post_balance) + + pre_nonce = pre_account.nonce if pre_account else Uint(0) + post_nonce = post_account.nonce if post_account else Uint(0) + if pre_nonce != post_nonce: + add_nonce_change(builder, address, idx, U64(post_nonce)) + + pre_code_hash = ( + pre_account.code_hash if pre_account else EMPTY_CODE_HASH + ) + post_code_hash = ( + post_account.code_hash if post_account else EMPTY_CODE_HASH + ) + if pre_code_hash != post_code_hash: + post_code = get_code(tx_state, post_code_hash) + add_code_change(builder, address, idx, post_code) + + # Compare storage writes against block cumulative state + for address, slots in tx_state.storage_writes.items(): + for key, post_value in slots.items(): + pre_value = _get_pre_tx_storage( + block_state.storage_writes, pre_state, address, key + ) + if pre_value != post_value: + # Convert slot from internal Bytes32 format to U256 for BAL. + # EIP-7928 uses U256 as it's more space-efficient in RLP. + u256_slot = U256.from_be_bytes(key) + add_storage_write(builder, address, u256_slot, idx, post_value) + + +def build_block_access_list( + builder: BlockAccessListBuilder, + block_state: BlockState, +) -> BlockAccessList: + """ + Build a [`BlockAccessList`] from the builder and block state. + + Feed accumulated reads from the block state into the builder, then produce + the final sorted and encoded block access list. + + [`BlockAccessList`]: ref:ethereum.forks.bogota.block_access_lists.BlockAccessList + """ # noqa: E501 + # Add storage reads (convert Bytes32 to U256 for BAL encoding) + for address, slot in block_state.storage_reads: + add_storage_read(builder, address, U256.from_be_bytes(slot)) + + # Add touched addresses + for address in block_state.account_reads: + add_touched_account(builder, address) + + return _build_from_builder(builder) + + +def hash_block_access_list( + block_access_list: BlockAccessList, +) -> Hash32: + """ + Compute the hash of a Block Access List. + """ + return keccak256(rlp.encode(block_access_list)) + + +def validate_block_access_list_gas_limit( + block_access_list: BlockAccessList, + block_gas_limit: Uint, +) -> None: + """ + Validate that the block access list does not exceed the gas limit. + + The total number of items (addresses + unique storage keys) must not + exceed ``block_gas_limit // GAS_BLOCK_ACCESS_LIST_ITEM``. + """ + from .vm.gas import GasCosts + + bal_items = Uint(0) + for account in block_access_list: + # Count each address as one item + bal_items += Uint(1) + + # Collect unique storage keys across both + # reads and writes + unique_slots: Set[U256] = set() + for slot_change in account.storage_changes: + unique_slots.add(slot_change.slot) + for slot in account.storage_reads: + unique_slots.add(slot) + + # Count each unique storage key as one item + bal_items += ulen(unique_slots) + + if bal_items > block_gas_limit // GasCosts.BLOCK_ACCESS_LIST_ITEM: + raise BlockAccessListGasLimitExceededError( + f"Block access list exceeds gas limit, {bal_items} items " + f"exceeds limit of " + f"{block_gas_limit // GasCosts.BLOCK_ACCESS_LIST_ITEM}." + ) diff --git a/src/ethereum/forks/bogota/blocks.py b/src/ethereum/forks/bogota/blocks.py new file mode 100644 index 00000000000..558ca2da7ae --- /dev/null +++ b/src/ethereum/forks/bogota/blocks.py @@ -0,0 +1,435 @@ +""" +A `Block` is a single link in the chain that is Ethereum. Each `Block` contains +a `Header` and zero or more transactions. Each `Header` contains associated +metadata like the block number, parent block hash, and how much gas was +consumed by its transactions. + +Together, these blocks form a cryptographically secure journal recording the +history of all state transitions that have happened since the genesis of the +chain. +""" + +from dataclasses import dataclass +from typing import Tuple, final + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes8, Bytes32 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.hash import Hash32 +from ethereum.state import Address, Root + +from .fork_types import Bloom +from .transactions import ( + AccessListTransaction, + BlobTransaction, + FeeMarketTransaction, + LegacyTransaction, + SetCodeTransaction, + Transaction, +) + + +@final +@slotted_freezable +@dataclass +class Withdrawal: + """ + Withdrawals represent a transfer of ETH from the consensus layer (beacon + chain) to the execution layer, as validated by the consensus layer. Each + withdrawal is listed in the block's list of withdrawals. See [`block`]. + + [`block`]: ref:ethereum.forks.bogota.blocks.Block.withdrawals + """ + + index: U64 + """ + The unique index of the withdrawal, incremented for each withdrawal + processed. + """ + + validator_index: U64 + """ + The index of the validator on the consensus layer that is withdrawing. + """ + + address: Address + """ + The execution-layer address receiving the withdrawn ETH. + """ + + amount: U256 + """ + The amount of ETH being withdrawn. + """ + + +@final +@slotted_freezable +@dataclass +class Header: + """ + Header portion of a block on the chain, containing metadata and + cryptographic commitments to the block's contents. + """ + + parent_hash: Hash32 + """ + Hash ([`keccak256`]) of the parent block's header, encoded with [RLP]. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [RLP]: https://ethereum.github.io/ethereum-rlp/src/ethereum_rlp/rlp.py.html + """ + + ommers_hash: Hash32 + """ + Hash ([`keccak256`]) of the ommers (uncle blocks) in this block, encoded + with [RLP]. However, in post merge forks `ommers_hash` is always + [`EMPTY_OMMER_HASH`]. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [RLP]: https://ethereum.github.io/ethereum-rlp/src/ethereum_rlp/rlp.py.html + [`EMPTY_OMMER_HASH`]: ref:ethereum.forks.bogota.fork.EMPTY_OMMER_HASH + """ + + coinbase: Address + """ + Address of the miner (or validator) who mined this block. + + The coinbase address receives the block reward and the priority fees (tips) + from included transactions. Base fees (introduced in [EIP-1559]) are burned + and do not go to the coinbase. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + """ + + state_root: Root + """ + Root hash ([`keccak256`]) of the state trie after executing all + transactions in this block. It represents the state of the Ethereum Virtual + Machine (EVM) after all transactions in this block have been processed. It + is computed using [`compute_state_root_and_trie_changes()`][changes], + which computes the root of the Merkle-Patricia [Trie] representing the + Ethereum world state after applying the block's state changes. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [changes]: ref:ethereum.state.State.compute_state_root_and_trie_changes + [Trie]: ref:ethereum.merkle_patricia_trie.Trie + """ + + transactions_root: Root + """ + Root hash ([`keccak256`]) of the transactions trie, which contains all + transactions included in this block in their original order. It is computed + using the [`root()`] function over the Merkle-Patricia [trie] of + transactions as the parameter. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [`root()`]: ref:ethereum.merkle_patricia_trie.root + [Trie]: ref:ethereum.merkle_patricia_trie.Trie + """ + + receipt_root: Root + """ + Root hash ([`keccak256`]) of the receipts trie, which contains all receipts + for transactions in this block. It is computed using the [`root()`] + function over the Merkle-Patricia [trie] constructed from the receipts. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [`root()`]: ref:ethereum.merkle_patricia_trie.root + [Trie]: ref:ethereum.merkle_patricia_trie.Trie + """ + + bloom: Bloom + """ + Bloom filter for logs generated by transactions in this block. + Constructed from all logs in the block using the [logs bloom] mechanism. + + [logs bloom]: ref:ethereum.forks.bogota.bloom.logs_bloom + """ + + difficulty: Uint + """ + Difficulty of the block (pre-PoS), or a constant in PoS. + """ + + number: Uint + """ + Block number (height) in the chain. + """ + + gas_limit: Uint + """ + Maximum gas allowed in this block. Pre [EIP-1559], this was the maximum + gas that could be consumed by all transactions in the block. Post + [EIP-1559], this is still the maximum gas limit, but the base fee per gas + is adjusted so that effective block gas utilization targets 50% of + that limit. The gas_limit is a voted parameter that can be + [adjusted by a factor of 1/1024] from the previous block's limit by the + block proposer, allowing the network to coordinate on capacity + increases (e.g. the 60M limit proposed in EIP-7935). + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + [adjusted by a factor of 1/1024]: + https://ethereum.org/en/developers/docs/blocks/ + """ + + gas_used: Uint + """ + Total gas used by all transactions in this block. + """ + + timestamp: U256 + """ + Timestamp of when the block was mined, in seconds since the unix epoch. + """ + + extra_data: Bytes + """ + Arbitrary data included by the miner. + """ + + prev_randao: Bytes32 + """ + Output of the RANDAO beacon for random validator selection. + """ + + nonce: Bytes8 + """ + Nonce used in the mining process (pre-PoS), set to zero in PoS. + """ + + base_fee_per_gas: Uint + """ + Base fee per gas for transactions in this block, introduced in + [EIP-1559]. This is the minimum fee per gas that must be paid for a + transaction to be included in this block. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + """ + + withdrawals_root: Root + """ + Root hash of the withdrawals trie, which contains all withdrawals in this + block. + """ + + blob_gas_used: U64 + """ + Total blob gas consumed by the transactions within this block. Introduced + in [EIP-4844]. + + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + """ + + excess_blob_gas: U64 + """ + Running total of blob gas consumed in excess of the target, prior to this + block. Blocks with above-target blob gas consumption increase this value, + while blocks with below-target blob gas consumption decrease it (to a + minimum of zero). Introduced in [EIP-4844]. + + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + """ + + parent_beacon_block_root: Root + """ + Root hash of the corresponding beacon chain block. + """ + + requests_hash: Hash32 + """ + [SHA2-256] hash of all the collected requests in this block. Introduced in + [EIP-7685]. See [`compute_requests_hash`][crh] for more details. + + [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 + [crh]: ref:ethereum.forks.bogota.requests.compute_requests_hash + [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 + """ + + block_access_list_hash: Hash32 + """ + [`keccak256`] hash of the Block Access List containing all accounts and + storage locations accessed during block execution. Introduced in + [EIP-7928]. See [`hash_block_access_list`][cbalh] for more + details. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 + [cbalh]: ref:ethereum.forks.bogota.block_access_lists.hash_block_access_list + """ # noqa: E501 + + slot_number: U64 + """ + The slot number of this block as provided by the consensus layer. + Introduced in [EIP-7843]. + + [EIP-7843]: https://eips.ethereum.org/EIPS/eip-7843 + """ + + +@final +@slotted_freezable +@dataclass +class Block: + """ + A complete block on Ethereum, which is composed of a block [`header`], + a list of transactions, a list of ommers (deprecated), and a list of + validator [withdrawals]. + + The block [`header`] includes fields relevant to the Proof-of-Stake + consensus, with deprecated Proof-of-Work fields such as `difficulty`, + `nonce`, and `ommersHash` set to constants. The `coinbase` field + denotes the address receiving priority fees from the block. + + The header also contains commitments to the current state (`stateRoot`), + the transactions (`transactionsRoot`), the transaction receipts + (`receiptsRoot`), and `withdrawalsRoot` committing to the validator + withdrawals included in this block. It also includes a bloom filter which + summarizes log data from the transactions. + + Withdrawals represent ETH transfers from validators to their recipients, + introduced by the consensus layer. Ommers remain deprecated and empty. + + [`header`]: ref:ethereum.forks.bogota.blocks.Header + [withdrawals]: ref:ethereum.forks.bogota.blocks.Withdrawal + """ + + header: Header + """ + The block header containing metadata and cryptographic commitments. Refer + to [headers] for more details on the fields included in the header. + + [headers]: ref:ethereum.forks.bogota.blocks.Header + """ + + transactions: Tuple[Bytes | LegacyTransaction, ...] + """ + A tuple of transactions included in this block. Each transaction can be + any of a legacy transaction, an access list transaction, a fee market + transaction, a blob transaction, or a set code transaction. + """ + + ommers: Tuple[Header, ...] + """ + A tuple of ommers (uncle blocks) included in this block. Always empty in + Proof-of-Stake forks. + """ + + withdrawals: Tuple[Withdrawal, ...] + """ + A tuple of withdrawals processed in this block. + """ + + +@final +@slotted_freezable +@dataclass +class Log: + """ + Data record produced during the execution of a transaction. Logs are used + by smart contracts to emit events (using the EVM log opcodes ([`LOG0`], + [`LOG1`], [`LOG2`], [`LOG3`] and [`LOG4`]), which can be efficiently + searched using the bloom filter in the block header. + + [`LOG0`]: ref:ethereum.forks.bogota.vm.instructions.log.log0 + [`LOG1`]: ref:ethereum.forks.bogota.vm.instructions.log.log1 + [`LOG2`]: ref:ethereum.forks.bogota.vm.instructions.log.log2 + [`LOG3`]: ref:ethereum.forks.bogota.vm.instructions.log.log3 + [`LOG4`]: ref:ethereum.forks.bogota.vm.instructions.log.log4 + """ + + address: Address + """ + The address of the contract that emitted the log. + """ + + topics: Tuple[Hash32, ...] + """ + A tuple of up to four topics associated with the log, used for filtering. + """ + + data: Bytes + """ + The data payload of the log, which can contain any arbitrary data. + """ + + +@final +@slotted_freezable +@dataclass +class Receipt: + """ + Result of a transaction execution. Receipts are included in the receipts + trie. + """ + + succeeded: bool + """ + Whether the transaction execution was successful. + """ + + cumulative_gas_used: Uint + """ + Total gas used in the block up to and including this transaction. + This is the gas used after refunds, paid by the user. + """ + + bloom: Bloom + """ + Bloom filter for logs generated by this transaction. This is a 2048-byte + bit array that allows for efficient filtering of logs. + """ + + logs: Tuple[Log, ...] + """ + A tuple of logs generated by this transaction. Each log contains the + address of the contract that emitted it, a tuple of topics, and the data + payload. + """ + + +def encode_receipt(tx: Transaction, receipt: Receipt) -> Bytes | Receipt: + r""" + Encodes a transaction receipt based on the transaction type. + + The encoding follows the same format as transactions encoding, where: + - AccessListTransaction receipts are prefixed with `b"\x01"`. + - FeeMarketTransaction receipts are prefixed with `b"\x02"`. + - BlobTransaction receipts are prefixed with `b"\x03"`. + - SetCodeTransaction receipts are prefixed with `b"\x04"`. + - LegacyTransaction receipts are returned as is. + """ + if isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(receipt) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(receipt) + elif isinstance(tx, BlobTransaction): + return b"\x03" + rlp.encode(receipt) + elif isinstance(tx, SetCodeTransaction): + return b"\x04" + rlp.encode(receipt) + else: + return receipt + + +def decode_receipt(receipt: Bytes | Receipt) -> Receipt: + r""" + Decodes a receipt from its serialized form. + + The decoding follows the same format as transactions decoding, where: + - Receipts prefixed with `b"\x01"` are decoded as AccessListTransaction + receipts. + - Receipts prefixed with `b"\x02"` are decoded as FeeMarketTransaction + receipts. + - Receipts prefixed with `b"\x03"` are decoded as BlobTransaction + receipts. + - Receipts prefixed with `b"\x04"` are decoded as SetCodeTransaction + receipts. + - LegacyTransaction receipts are returned as is. + """ + if isinstance(receipt, Bytes): + assert receipt[0] in (1, 2, 3, 4) + return rlp.decode_to(Receipt, receipt[1:]) + else: + return receipt diff --git a/src/ethereum/forks/bogota/bloom.py b/src/ethereum/forks/bogota/bloom.py new file mode 100644 index 00000000000..0e079df6b39 --- /dev/null +++ b/src/ethereum/forks/bogota/bloom.py @@ -0,0 +1,87 @@ +""" +Ethereum Logs Bloom. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +This module defines functions for calculating bloom filters of logs. For the +general theory of bloom filters see e.g. `Wikipedia +`_. Bloom filters are used to allow +for efficient searching of logs by address and/or topic, by rapidly +eliminating blocks and receipts from their search. +""" + +from typing import Tuple + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import Uint + +from ethereum.crypto.hash import keccak256 + +from .blocks import Log +from .fork_types import Bloom + + +def add_to_bloom(bloom: bytearray, bloom_entry: Bytes) -> None: + """ + Add a bloom entry to the bloom filter (`bloom`). + + The number of hash functions used is 3. They are calculated by taking the + least significant 11 bits from the first 3 16-bit words of the + `keccak_256()` hash of `bloom_entry`. + + Parameters + ---------- + bloom : + The bloom filter. + bloom_entry : + An entry which is to be added to bloom filter. + + """ + hashed = keccak256(bloom_entry) + + for idx in (0, 2, 4): + # Obtain the least significant 11 bits from the pair of bytes + # (16 bits), and set this bit in bloom bytearray. + # The obtained bit is 0-indexed in the bloom filter from the least + # significant bit to the most significant bit. + bit_to_set = Uint.from_be_bytes(hashed[idx : idx + 2]) & Uint(0x07FF) + # Below is the index of the bit in the bytearray (where 0-indexed + # byte is the most significant byte) + bit_index = 0x07FF - int(bit_to_set) + + byte_index = bit_index // 8 + bit_value = 1 << (7 - (bit_index % 8)) + bloom[byte_index] = bloom[byte_index] | bit_value + + +def logs_bloom(logs: Tuple[Log, ...]) -> Bloom: + """ + Obtain the logs bloom from a list of log entries. + + The address and each topic of a log are added to the bloom filter. + + Parameters + ---------- + logs : + List of logs for which the logs bloom is to be obtained. + + Returns + ------- + logs_bloom : `Bloom` + The logs bloom obtained which is 256 bytes with some bits set as per + the caller address and the log topics. + + """ + bloom: bytearray = bytearray(b"\x00" * 256) + + for log in logs: + add_to_bloom(bloom, log.address) + for topic in log.topics: + add_to_bloom(bloom, topic) + + return Bloom(bloom) diff --git a/src/ethereum/forks/bogota/exceptions.py b/src/ethereum/forks/bogota/exceptions.py new file mode 100644 index 00000000000..6ef5651cfa8 --- /dev/null +++ b/src/ethereum/forks/bogota/exceptions.py @@ -0,0 +1,155 @@ +""" +Exceptions specific to this fork. +""" + +from typing import TYPE_CHECKING, Final + +from ethereum_types.numeric import U64, Uint + +from ethereum.exceptions import InvalidBlock, InvalidTransaction + +if TYPE_CHECKING: + from .transactions import Transaction + + +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + +class TransactionTypeError(InvalidTransaction): + """ + Unknown [EIP-2718] transaction type byte. + + [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + """ + + transaction_type: Final[int] + """ + The type byte of the transaction that caused the error. + """ + + def __init__(self, transaction_type: int): + super().__init__(f"unknown transaction type `{transaction_type}`") + self.transaction_type = transaction_type + + +class TransactionTypeContractCreationError(InvalidTransaction): + """ + Contract creation is not allowed for a transaction type. + """ + + transaction: "Transaction" + """ + The transaction that caused the error. + """ + + def __init__(self, transaction: "Transaction"): + super().__init__( + f"transaction type `{type(transaction).__name__}` not allowed to " + "create contracts" + ) + self.transaction = transaction + + +class BlobGasLimitExceededError(InvalidTransaction): + """ + The blob gas limit for the transaction exceeds the maximum allowed. + """ + + +class InsufficientMaxFeePerBlobGasError(InvalidTransaction): + """ + The maximum fee per blob gas is insufficient for the transaction. + """ + + +class InsufficientMaxFeePerGasError(InvalidTransaction): + """ + The maximum fee per gas is insufficient for the transaction. + """ + + transaction_max_fee_per_gas: Final[Uint] + """ + The maximum fee per gas specified in the transaction. + """ + + block_base_fee_per_gas: Final[Uint] + """ + The base fee per gas of the block in which the transaction is included. + """ + + def __init__( + self, transaction_max_fee_per_gas: Uint, block_base_fee_per_gas: Uint + ): + super().__init__( + f"Insufficient max fee per gas " + f"({transaction_max_fee_per_gas} < {block_base_fee_per_gas})" + ) + self.transaction_max_fee_per_gas = transaction_max_fee_per_gas + self.block_base_fee_per_gas = block_base_fee_per_gas + + +class InvalidBlobVersionedHashError(InvalidTransaction): + """ + The versioned hash of the blob is invalid. + """ + + +class NoBlobDataError(InvalidTransaction): + """ + The transaction does not contain any blob data. + """ + + +class BlobCountExceededError(InvalidTransaction): + """ + The transaction has more blobs than the limit. + """ + + +class PriorityFeeGreaterThanMaxFeeError(InvalidTransaction): + """ + The priority fee is greater than the maximum fee per gas. + """ + + +class EmptyAuthorizationListError(InvalidTransaction): + """ + The authorization list in the transaction is empty. + """ + + +class InitCodeTooLargeError(InvalidTransaction): + """ + The init code of the transaction is too large. + """ + + +class TransactionGasLimitExceededError(InvalidTransaction): + """ + The transaction has specified a gas limit that is greater than the allowed + maximum. + + Note that this is _not_ the exception thrown when bytecode execution runs + out of gas. + """ + + +class BlockAccessListGasLimitExceededError(InvalidBlock): + """ + The block access list exceeds the gas limit constraint. + + Introduced in [EIP-7928]. + + [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 + """ diff --git a/src/ethereum/forks/bogota/fork.py b/src/ethereum/forks/bogota/fork.py new file mode 100644 index 00000000000..85fca973be1 --- /dev/null +++ b/src/ethereum/forks/bogota/fork.py @@ -0,0 +1,1238 @@ +""" +Ethereum Specification. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Entry point for the Ethereum specification. +""" + +from dataclasses import dataclass +from typing import Final, List, Optional, Tuple, final + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U64, U256, Uint, ulen + +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import ( + EthereumException, + GasUsedExceedsLimitError, + InsufficientBalanceError, + InvalidBlock, + InvalidSenderError, + NonceMismatchError, +) +from ethereum.forks.amsterdam.blocks import Header as PreviousHeader +from ethereum.merkle_patricia_trie import root, trie_set +from ethereum.state import ( + EMPTY_CODE_HASH, + Address, + BlockDiff, + State, + apply_changes_to_state, +) + +from . import vm +from .block_access_lists import ( + BlockAccessListBuilder, + build_block_access_list, + hash_block_access_list, + validate_block_access_list_gas_limit, +) +from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt +from .bloom import logs_bloom +from .exceptions import ( + BlobCountExceededError, + BlobGasLimitExceededError, + EmptyAuthorizationListError, + InsufficientMaxFeePerBlobGasError, + InsufficientMaxFeePerGasError, + InvalidBlobVersionedHashError, + NoBlobDataError, + PriorityFeeGreaterThanMaxFeeError, + TransactionTypeContractCreationError, + WrongChainIdError, +) +from .fork_types import Authorization, BlockAccessIndex, VersionedHash +from .requests import ( + BUILDER_DEPOSIT_REQUEST_TYPE, + BUILDER_EXIT_REQUEST_TYPE, + CONSOLIDATION_REQUEST_TYPE, + DEPOSIT_REQUEST_TYPE, + WITHDRAWAL_REQUEST_TYPE, + compute_requests_hash, + parse_deposit_requests, +) +from .state_tracker import ( + BlockState, + TransactionState, + clear_account_preserving_balance, + create_ether, + extract_block_diff, + get_account, + get_code, + incorporate_tx_into_block, + increment_nonce, + set_account_balance, +) +from .transactions import ( + TX_MAX_GAS_LIMIT, + BlobTransaction, + FeeMarketCapableTransaction, + LegacyTransaction, + SetCodeTransaction, + Transaction, + chain_id, + decode_transaction, + encode_transaction, + get_transaction_hash, + has_access_list, + recover_sender, + validate_transaction, +) +from .utils.hexadecimal import hex_to_address +from .utils.message import prepare_message +from .vm import Message +from .vm.eoa_delegation import is_valid_delegation +from .vm.gas import ( + GasCosts, + StateGasCosts, + calculate_blob_gas_price, + calculate_data_fee, + calculate_excess_blob_gas, + calculate_total_blob_gas, +) +from .vm.interpreter import MessageCallOutput, process_message_call + +BASE_FEE_MAX_CHANGE_DENOMINATOR = Uint(8) +ELASTICITY_MULTIPLIER = Uint(2) +EMPTY_OMMER_HASH = keccak256(rlp.encode([])) +SYSTEM_ADDRESS = hex_to_address("0xfffffffffffffffffffffffffffffffffffffffe") +BEACON_ROOTS_ADDRESS = hex_to_address( + "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02" +) +SYSTEM_TRANSACTION_GAS = Uint(30000000) +SYSTEM_MAX_SSTORES_PER_CALL = Uint(16) +""" +Upper bound on the number of new storage slots a single system call is +expected to write. +""" +MAX_BLOB_GAS_PER_BLOCK: Final[U64] = ( + GasCosts.BLOB_SCHEDULE_MAX * GasCosts.PER_BLOB +) +VERSIONED_HASH_VERSION_KZG = b"\x01" +GWEI_TO_WEI = U256(10**9) + +WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( + "0x00000961Ef480Eb55e80D19ad83579A64c007002" +) +CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( + "0x0000BBdDc7CE488642fb579F8B00f3a590007251" +) +BUILDER_DEPOSIT_CONTRACT_ADDRESS = hex_to_address( + "0x0000884d2AA32eAa155F59A2f24eFa73D9008282" +) +BUILDER_EXIT_CONTRACT_ADDRESS = hex_to_address( + "0x000014574A74c805590AFF9499fc7A690f008282" +) +HISTORY_STORAGE_ADDRESS = hex_to_address( + "0x0000F90827F1C53a10cb7A02335B175320002935" +) +MAX_BLOCK_SIZE = 10_485_760 +SAFETY_MARGIN = 2_097_152 +MAX_RLP_BLOCK_SIZE = MAX_BLOCK_SIZE - SAFETY_MARGIN +BLOB_COUNT_LIMIT = 6 + + +@final +@slotted_freezable +@dataclass +class ChainContext: + """ + Chain context needed for block execution. + """ + + chain_id: U64 + """Identify the chain for transaction signature recovery.""" + + block_hashes: List[Hash32] + """Recent ancestor hashes (up to 256) for the ``BLOCKHASH`` opcode.""" + + parent_header: Header | PreviousHeader + """Parent header used for header validation and system contracts.""" + + +@final +@dataclass +class BlockChain: + """ + History and current state of the block chain. + """ + + blocks: List[Block] + state: State + chain_id: U64 + + +def apply_fork(old: BlockChain) -> BlockChain: + """ + Transforms the state from the previous hard fork (`old`) into the block + chain object for this hard fork and returns it. + + When forks need to implement an irregular state transition, this function + is used to handle the irregularity. See the :ref:`DAO Fork ` for + an example. + + Parameters + ---------- + old : + Previous block chain object. + + Returns + ------- + new : `BlockChain` + Upgraded block chain object for this hard fork. + + """ + return old + + +def get_last_256_block_hashes(chain: BlockChain) -> List[Hash32]: + """ + Obtain the list of hashes of the previous 256 blocks in order of + increasing block number. + + This function will return less hashes for the first 256 blocks. + + The ``BLOCKHASH`` opcode needs to access the latest hashes on the chain, + therefore this function retrieves them. + + Parameters + ---------- + chain : + History and current state. + + Returns + ------- + recent_block_hashes : `List[Hash32]` + Hashes of the recent 256 blocks in order of increasing block number. + + """ + recent_blocks = chain.blocks[-255:] + # TODO: This function has not been tested rigorously + if len(recent_blocks) == 0: + return [] + + recent_block_hashes = [] + + for block in recent_blocks: + prev_block_hash = block.header.parent_hash + recent_block_hashes.append(prev_block_hash) + + # We are computing the hash only for the most recent block and not for + # the rest of the blocks as they have successors which have the hash of + # the current block as parent hash. + most_recent_block_hash = keccak256(rlp.encode(recent_blocks[-1].header)) + recent_block_hashes.append(most_recent_block_hash) + + return recent_block_hashes + + +def state_transition(chain: BlockChain, block: Block) -> None: + """ + Attempts to apply a block to an existing block chain. + + All parts of the block's contents need to be verified before being added + to the chain. Blocks are verified by ensuring that the contents of the + block make logical sense with the contents of the parent block. The + information in the block's header must also match the corresponding + information in the block. + + To implement Ethereum, in theory clients are only required to store the + most recent 255 blocks of the chain since as far as execution is + concerned, only those blocks are accessed. Practically, however, clients + should store more blocks to handle reorgs. + + Parameters + ---------- + chain : + History and current state. + block : + Block to apply to `chain`. + + """ + chain_context = ChainContext( + chain_id=chain.chain_id, + block_hashes=get_last_256_block_hashes(chain), + parent_header=chain.blocks[-1].header, + ) + + block_diff = execute_block(block, chain.state, chain_context) + + apply_changes_to_state(chain.state, block_diff) + chain.blocks.append(block) + if len(chain.blocks) > 255: + # Real clients have to store more blocks to deal with reorgs, but the + # protocol only requires the last 255 + chain.blocks = chain.blocks[-255:] + + +def execute_block( + block: Block, + pre_state: State, + chain_context: ChainContext, +) -> BlockDiff: + """ + Execute a block and validate the resulting roots against the header. + + This method is idempotent. + + Parameters + ---------- + block : + Block to validate and execute. + pre_state : + Pre-execution state provider. + chain_context : + Chain context that the block may need during execution. + + Returns + ------- + block_diff : `BlockDiff` + Account, storage, and code changes produced by block execution. + + """ + if len(rlp.encode(block)) > MAX_RLP_BLOCK_SIZE: + raise InvalidBlock("Block rlp size exceeds MAX_RLP_BLOCK_SIZE") + + parent_header = chain_context.parent_header + validate_header(parent_header, block.header) + + if block.ommers != (): + raise InvalidBlock + + block_state = BlockState(pre_state=pre_state) + + block_env = vm.BlockEnvironment( + chain_id=chain_context.chain_id, + state=block_state, + block_gas_limit=block.header.gas_limit, + block_hashes=chain_context.block_hashes, + coinbase=block.header.coinbase, + number=block.header.number, + base_fee_per_gas=block.header.base_fee_per_gas, + time=block.header.timestamp, + prev_randao=block.header.prev_randao, + excess_blob_gas=block.header.excess_blob_gas, + parent_beacon_block_root=block.header.parent_beacon_block_root, + block_access_list_builder=BlockAccessListBuilder(), + slot_number=block.header.slot_number, + ) + + block_output = apply_body( + block_env=block_env, + transactions=block.transactions, + withdrawals=block.withdrawals, + ) + block_diff = extract_block_diff(block_state) + block_state_root, _ = pre_state.compute_state_root_and_trie_changes( + block_diff.account_changes, block_diff.storage_changes + ) + transactions_root = root(block_output.transactions_trie) + receipt_root = root(block_output.receipts_trie) + block_logs_bloom = logs_bloom(block_output.block_logs) + withdrawals_root = root(block_output.withdrawals_trie) + requests_hash = compute_requests_hash(block_output.requests) + computed_block_access_list_hash = hash_block_access_list( + block_output.block_access_list + ) + + 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: + raise InvalidBlock + if receipt_root != block.header.receipt_root: + raise InvalidBlock + if block_logs_bloom != block.header.bloom: + raise InvalidBlock + if withdrawals_root != block.header.withdrawals_root: + raise InvalidBlock + if block_output.blob_gas_used != block.header.blob_gas_used: + raise InvalidBlock + if requests_hash != block.header.requests_hash: + raise InvalidBlock + if computed_block_access_list_hash != block.header.block_access_list_hash: + raise InvalidBlock("Invalid block access list hash") + + return block_diff + + +def calculate_base_fee_per_gas( + block_gas_limit: Uint, + parent_gas_limit: Uint, + parent_gas_used: Uint, + parent_base_fee_per_gas: Uint, +) -> Uint: + """ + Calculates the base fee per gas for the block. + + Parameters + ---------- + block_gas_limit : + Gas limit of the block for which the base fee is being calculated. + parent_gas_limit : + Gas limit of the parent block. + parent_gas_used : + Gas used in the parent block. + parent_base_fee_per_gas : + Base fee per gas of the parent block. + + Returns + ------- + base_fee_per_gas : `Uint` + Base fee per gas for the block. + + """ + parent_gas_target = parent_gas_limit // ELASTICITY_MULTIPLIER + if not check_gas_limit(block_gas_limit, parent_gas_limit): + raise InvalidBlock + + if parent_gas_used == parent_gas_target: + expected_base_fee_per_gas = parent_base_fee_per_gas + elif parent_gas_used > parent_gas_target: + gas_used_delta = parent_gas_used - parent_gas_target + + parent_fee_gas_delta = parent_base_fee_per_gas * gas_used_delta + target_fee_gas_delta = parent_fee_gas_delta // parent_gas_target + + base_fee_per_gas_delta = max( + target_fee_gas_delta // BASE_FEE_MAX_CHANGE_DENOMINATOR, + Uint(1), + ) + + expected_base_fee_per_gas = ( + parent_base_fee_per_gas + base_fee_per_gas_delta + ) + else: + gas_used_delta = parent_gas_target - parent_gas_used + + parent_fee_gas_delta = parent_base_fee_per_gas * gas_used_delta + target_fee_gas_delta = parent_fee_gas_delta // parent_gas_target + + base_fee_per_gas_delta = ( + target_fee_gas_delta // BASE_FEE_MAX_CHANGE_DENOMINATOR + ) + + expected_base_fee_per_gas = ( + parent_base_fee_per_gas - base_fee_per_gas_delta + ) + + return Uint(expected_base_fee_per_gas) + + +def validate_header( + parent_header: Header | PreviousHeader, header: Header +) -> None: + """ + Verify a block header against its parent. + + In order to consider a block's header valid, the logic for the + quantities in the header should match the logic for the block itself. + For example the header timestamp should be greater than the block's parent + timestamp because the block was created *after* the parent block. + Additionally, the block's number should be directly following the parent + block's number since it is the next block in the sequence. + + Parameters + ---------- + parent_header : + Header of the parent block. + header : + Header to check for correctness. + + """ + if header.number < Uint(1): + raise InvalidBlock + + excess_blob_gas = calculate_excess_blob_gas(parent_header) + if header.excess_blob_gas != excess_blob_gas: + raise InvalidBlock + + if header.gas_used > header.gas_limit: + raise InvalidBlock + + expected_base_fee_per_gas = calculate_base_fee_per_gas( + header.gas_limit, + parent_header.gas_limit, + parent_header.gas_used, + parent_header.base_fee_per_gas, + ) + if expected_base_fee_per_gas != header.base_fee_per_gas: + raise InvalidBlock + if header.timestamp <= parent_header.timestamp: + raise InvalidBlock + if header.number != parent_header.number + Uint(1): + raise InvalidBlock + if len(header.extra_data) > 32: + raise InvalidBlock + if header.difficulty != 0: + raise InvalidBlock + if header.nonce != b"\x00\x00\x00\x00\x00\x00\x00\x00": + raise InvalidBlock + if header.ommers_hash != EMPTY_OMMER_HASH: + raise InvalidBlock + + block_parent_hash = keccak256(rlp.encode(parent_header)) + if header.parent_hash != block_parent_hash: + raise InvalidBlock + + +def check_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + sender: Address, + tx_state: TransactionState, +) -> Tuple[Uint, Tuple[VersionedHash, ...], U64]: + """ + Check if the transaction is includable in the block. + + Parameters + ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. + tx : + The transaction. + sender : + The recovered sender address of the transaction. + tx_state : + The transaction state tracker. + + Returns + ------- + effective_gas_price : + The price to charge for gas when the transaction is executed. + blob_versioned_hashes : + The blob versioned hashes of the transaction. + tx_blob_gas_used: + The blob gas used by the transaction. + + Raises + ------ + InvalidBlock : + If the transaction is not includable. + GasUsedExceedsLimitError : + If the gas used by the transaction exceeds the block's gas limit. + NonceMismatchError : + If the nonce of the transaction is not equal to the sender's nonce. + InsufficientBalanceError : + If the sender's balance is not enough to pay for the transaction. + InvalidSenderError : + If the transaction is from an address that does not exist anymore. + PriorityFeeGreaterThanMaxFeeError : + If the priority fee is greater than the maximum fee per gas. + InsufficientMaxFeePerGasError : + If the maximum fee per gas is insufficient for the transaction. + InsufficientMaxFeePerBlobGasError : + If the maximum fee per blob gas is insufficient for the transaction. + BlobGasLimitExceededError : + If the blob gas used by the transaction exceeds the block's blob gas + limit. + InvalidBlobVersionedHashError : + If the transaction contains a blob versioned hash with an invalid + version. + NoBlobDataError : + If the transaction is a type 3 but has no blobs. + BlobCountExceededError : + If the transaction is a type 3 and has more blobs than the limit. + TransactionTypeContractCreationError: + If the transaction type is not allowed to create contracts. + EmptyAuthorizationListError : + If the transaction is a SetCodeTransaction and the authorization list + is empty. + + """ + regular_gas_available = ( + block_env.block_gas_limit - block_output.block_gas_used + ) + state_gas_available = ( + block_env.block_gas_limit - block_output.block_state_gas_used + ) + blob_gas_available = MAX_BLOB_GAS_PER_BLOCK - block_output.blob_gas_used + + # EIP-8037 per-dimension inclusion check. + if min(TX_MAX_GAS_LIMIT, tx.gas) > regular_gas_available: + raise GasUsedExceedsLimitError("regular gas used exceeds limit") + + if tx.gas > state_gas_available: + raise GasUsedExceedsLimitError("state gas used exceeds limit") + + tx_blob_gas_used = calculate_total_blob_gas(tx) + if tx_blob_gas_used > blob_gas_available: + raise BlobGasLimitExceededError("blob gas limit exceeded") + + sender_account = get_account(tx_state, sender) + + if isinstance(tx, FeeMarketCapableTransaction): + if tx.max_fee_per_gas < tx.max_priority_fee_per_gas: + raise PriorityFeeGreaterThanMaxFeeError( + "priority fee greater than max fee" + ) + if tx.max_fee_per_gas < block_env.base_fee_per_gas: + raise InsufficientMaxFeePerGasError( + tx.max_fee_per_gas, block_env.base_fee_per_gas + ) + + priority_fee_per_gas = min( + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas - block_env.base_fee_per_gas, + ) + effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas + max_gas_fee = tx.gas * tx.max_fee_per_gas + else: + if tx.gas_price < block_env.base_fee_per_gas: + raise InvalidBlock + effective_gas_price = tx.gas_price + max_gas_fee = tx.gas * tx.gas_price + + if isinstance(tx, BlobTransaction): + blob_count = len(tx.blob_versioned_hashes) + if blob_count == 0: + raise NoBlobDataError("no blob data in transaction") + if blob_count > BLOB_COUNT_LIMIT: + raise BlobCountExceededError( + f"Tx has {blob_count} blobs. Max allowed: {BLOB_COUNT_LIMIT}" + ) + for blob_versioned_hash in tx.blob_versioned_hashes: + if blob_versioned_hash[0:1] != VERSIONED_HASH_VERSION_KZG: + raise InvalidBlobVersionedHashError( + "invalid blob versioned hash" + ) + + blob_gas_price = calculate_blob_gas_price(block_env.excess_blob_gas) + if Uint(tx.max_fee_per_blob_gas) < blob_gas_price: + raise InsufficientMaxFeePerBlobGasError( + "insufficient max fee per blob gas" + ) + + max_gas_fee += Uint(calculate_total_blob_gas(tx)) * Uint( + tx.max_fee_per_blob_gas + ) + blob_versioned_hashes = tx.blob_versioned_hashes + else: + blob_versioned_hashes = () + + if isinstance(tx, (BlobTransaction, SetCodeTransaction)): + if not isinstance(tx.to, Address): + raise TransactionTypeContractCreationError(tx) + + if isinstance(tx, SetCodeTransaction): + if not any(tx.authorizations): + raise EmptyAuthorizationListError("empty authorization list") + + if sender_account.nonce > Uint(tx.nonce): + raise NonceMismatchError("nonce too low") + elif sender_account.nonce < Uint(tx.nonce): + raise NonceMismatchError("nonce too high") + + if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + raise InsufficientBalanceError("insufficient sender balance") + sender_code = get_code(tx_state, sender_account.code_hash) + if sender_account.code_hash != EMPTY_CODE_HASH and not is_valid_delegation( + sender_code + ): + raise InvalidSenderError("not EOA") + + return ( + effective_gas_price, + blob_versioned_hashes, + tx_blob_gas_used, + ) + + +def make_receipt( + tx: Transaction, + error: Optional[EthereumException], + cumulative_gas_used: Uint, + logs: Tuple[Log, ...], +) -> Bytes | Receipt: + """ + Make the receipt for a transaction that was executed. + + Parameters + ---------- + tx : + The executed transaction. + error : + Error in the top level frame of the transaction, if any. + cumulative_gas_used : + The total gas used so far in the block after the transaction was + executed. This is the gas used after refunds. + logs : + The logs produced by the transaction. + + Returns + ------- + receipt : + The receipt for the transaction. + + """ + receipt = Receipt( + succeeded=error is None, + cumulative_gas_used=cumulative_gas_used, + bloom=logs_bloom(logs), + logs=logs, + ) + + return encode_receipt(tx, receipt) + + +def process_checked_system_transaction( + block_env: vm.BlockEnvironment, + target_address: Address, + data: Bytes, +) -> MessageCallOutput: + """ + Process a system transaction and raise an error if the contract does not + contain code or if the transaction fails. + + Parameters + ---------- + block_env : + The block scoped environment. + target_address : + Address of the contract to call. + data : + Data to pass to the contract. + + Returns + ------- + system_tx_output : `MessageCallOutput` + Output of processing the system transaction. + + """ + # Pre-check that the system contract has code. We use a throwaway + # TransactionState here that is *never* propagated back to BlockState + # (no incorporate_tx_into_block call); the same get_account / get_code + # lookups are performed and properly tracked by + # process_unchecked_system_transaction below, which this function + # always calls. Reading via a TransactionState (rather than directly + # against pre_state) lets us see system contracts deployed earlier in + # the same block — see EIP-7002 and EIP-7251 for this edge case. + untracked_state = TransactionState(parent=block_env.state) + system_contract_code = get_code( + untracked_state, + get_account(untracked_state, target_address).code_hash, + ) + + if len(system_contract_code) == 0: + raise InvalidBlock( + f"System contract address {target_address.hex()} does not " + "contain code" + ) + + system_tx_output = process_unchecked_system_transaction( + block_env, + target_address, + data, + ) + + if system_tx_output.error: + raise InvalidBlock( + f"System contract ({target_address.hex()}) call failed: " + f"{system_tx_output.error}" + ) + + return system_tx_output + + +def process_unchecked_system_transaction( + block_env: vm.BlockEnvironment, + target_address: Address, + data: Bytes, +) -> MessageCallOutput: + """ + Process a system transaction without checking if the contract contains + code or if the transaction fails. + + Parameters + ---------- + block_env : + The block scoped environment. + target_address : + Address of the contract to call. + data : + Data to pass to the contract. + + Returns + ------- + system_tx_output : `MessageCallOutput` + Output of processing the system transaction. + + """ + system_tx_state = TransactionState(parent=block_env.state) + system_contract_code = get_code( + system_tx_state, + get_account(system_tx_state, target_address).code_hash, + ) + + tx_env = vm.TransactionEnvironment( + origin=SYSTEM_ADDRESS, + recipient=target_address, + value=U256(0), + gas_price=block_env.base_fee_per_gas, + gas=SYSTEM_TRANSACTION_GAS, + state_gas_reservoir=( + StateGasCosts.STORAGE_SET * SYSTEM_MAX_SSTORES_PER_CALL + ), + 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( + block_env=block_env, + tx_env=tx_env, + caller=SYSTEM_ADDRESS, + target=target_address, + gas=SYSTEM_TRANSACTION_GAS, + state_gas_reservoir=( + StateGasCosts.STORAGE_SET * SYSTEM_MAX_SSTORES_PER_CALL + ), + value=U256(0), + data=data, + code=system_contract_code, + depth=Uint(0), + current_target=target_address, + code_address=target_address, + should_transfer_value=False, + is_static=False, + accessed_addresses=set(), + accessed_storage_keys=set(), + disable_precompiles=False, + parent_evm=None, + ) + + system_tx_output = process_message_call(system_tx_message) + + incorporate_tx_into_block( + system_tx_state, block_env.block_access_list_builder + ) + + return system_tx_output + + +def apply_body( + block_env: vm.BlockEnvironment, + transactions: Tuple[LegacyTransaction | Bytes, ...], + withdrawals: Tuple[Withdrawal, ...], +) -> vm.BlockOutput: + """ + Executes a block. + + Many of the contents of a block are stored in data structures called + tries. There is a transactions trie which is similar to a ledger of the + transactions stored in the current block. There is also a receipts trie + which stores the results of executing a transaction, like the post state + and gas used. This function creates and executes the block that is to be + added to the chain. + + Parameters + ---------- + block_env : + The block scoped environment. + transactions : + Transactions included in the block. + withdrawals : + Withdrawals to be processed in the current block. + + Returns + ------- + block_output : + The block output for the current block. + + """ + block_output = vm.BlockOutput() + + process_unchecked_system_transaction( + block_env=block_env, + target_address=BEACON_ROOTS_ADDRESS, + data=block_env.parent_beacon_block_root, + ) + + process_unchecked_system_transaction( + block_env=block_env, + target_address=HISTORY_STORAGE_ADDRESS, + data=block_env.block_hashes[-1], # The parent hash + ) + + for i, tx in enumerate(map(decode_transaction, transactions)): + process_transaction(block_env, block_output, tx, Uint(i)) + + # EIP-7928: Post-execution operations use index N+1 + block_env.block_access_list_builder.block_access_index = BlockAccessIndex( + ulen(transactions) + Uint(1) + ) + + process_withdrawals(block_env, block_output, withdrawals) + + process_general_purpose_requests( + block_env=block_env, + block_output=block_output, + ) + + block_output.block_access_list = build_block_access_list( + block_env.block_access_list_builder, block_env.state + ) + + # Validate block access list gas limit constraint (EIP-7928) + validate_block_access_list_gas_limit( + block_access_list=block_output.block_access_list, + block_gas_limit=block_env.block_gas_limit, + ) + + return block_output + + +def process_general_purpose_requests( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, +) -> None: + """ + Process all the requests in the block. + + Parameters + ---------- + block_env : + The execution environment for the Block. + block_output : + The block output for the current block. + + """ + # Requests are to be in ascending order of request type + deposit_requests = parse_deposit_requests(block_output) + requests_from_execution = block_output.requests + if len(deposit_requests) > 0: + requests_from_execution.append(DEPOSIT_REQUEST_TYPE + deposit_requests) + + system_withdrawal_tx_output = process_checked_system_transaction( + block_env=block_env, + target_address=WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, + data=b"", + ) + + if len(system_withdrawal_tx_output.return_data) > 0: + requests_from_execution.append( + WITHDRAWAL_REQUEST_TYPE + system_withdrawal_tx_output.return_data + ) + + system_consolidation_tx_output = process_checked_system_transaction( + block_env=block_env, + target_address=CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, + data=b"", + ) + + if len(system_consolidation_tx_output.return_data) > 0: + requests_from_execution.append( + CONSOLIDATION_REQUEST_TYPE + + system_consolidation_tx_output.return_data + ) + + system_builder_deposit_tx_output = process_checked_system_transaction( + block_env=block_env, + target_address=BUILDER_DEPOSIT_CONTRACT_ADDRESS, + data=b"", + ) + + if len(system_builder_deposit_tx_output.return_data) > 0: + requests_from_execution.append( + BUILDER_DEPOSIT_REQUEST_TYPE + + system_builder_deposit_tx_output.return_data + ) + + system_builder_exit_tx_output = process_checked_system_transaction( + block_env=block_env, + target_address=BUILDER_EXIT_CONTRACT_ADDRESS, + data=b"", + ) + + if len(system_builder_exit_tx_output.return_data) > 0: + requests_from_execution.append( + BUILDER_EXIT_REQUEST_TYPE + + system_builder_exit_tx_output.return_data + ) + + +def process_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: Transaction, + index: Uint, +) -> None: + """ + Execute a transaction against the provided environment. + + This function processes the actions needed to execute a transaction. + It decrements the sender's account balance after calculating the gas fee + and refunds them the proper amount after execution. Calling contracts, + deploying code, and incrementing nonces are all examples of actions that + happen within this function or from a call made within this function. + + Accounts that are marked for deletion are processed and destroyed after + execution. + + Parameters + ---------- + block_env : + Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. + tx : + Transaction to execute. + index: + Index of the transaction in the block. + + """ + block_env.block_access_list_builder.block_access_index = BlockAccessIndex( + index + Uint(1) + ) + tx_state = TransactionState(parent=block_env.state) + + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) + + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender = recover_sender(tx) + intrinsic = validate_transaction(tx, sender) + + intrinsic_gas = Uint(intrinsic.regular) + Uint(intrinsic.state) + + ( + effective_gas_price, + blob_versioned_hashes, + tx_blob_gas_used, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + sender=sender, + tx_state=tx_state, + ) + + sender_account = get_account(tx_state, sender) + + if isinstance(tx, BlobTransaction): + blob_gas_fee = calculate_data_fee(block_env.excess_blob_gas, tx) + else: + blob_gas_fee = Uint(0) + + effective_gas_fee = tx.gas * effective_gas_price + + # 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) + + sender_balance_after_gas_fee = ( + Uint(sender_account.balance) - effective_gas_fee - blob_gas_fee + ) + set_account_balance(tx_state, sender, U256(sender_balance_after_gas_fee)) + + access_list_addresses = set() + access_list_storage_keys = set() + access_list_addresses.add(block_env.coinbase) + if has_access_list(tx): + for access in tx.access_list: + access_list_addresses.add(access.account) + for slot in access.slots: + access_list_storage_keys.add((access.account, slot)) + + authorizations: Tuple[Authorization, ...] = () + if isinstance(tx, SetCodeTransaction): + authorizations = tx.authorizations + + tx_env = vm.TransactionEnvironment( + origin=sender, + recipient=tx.to, + value=tx.value, + 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( + block_env, + tx_env, + tx, + ) + + tx_output = process_message_call(message) + + if isinstance(tx.to, Bytes0) and ( + tx_output.error is not None or tx_output.created_target_alive + ): + new_account_refund = StateGasCosts.NEW_ACCOUNT + tx_output.state_gas_left += new_account_refund + tx_output.state_refund += new_account_refund + + tx_gas_used_before_refund = ( + tx.gas - tx_output.gas_left - tx_output.state_gas_left + ) + 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 = max(tx_gas_used_after_refund, intrinsic.calldata_floor) + + tx_gas_left = tx.gas - tx_gas_used + gas_refund_amount = tx_gas_left * effective_gas_price + + # For non-1559 transactions effective_gas_price == tx.gas_price + priority_fee_per_gas = effective_gas_price - block_env.base_fee_per_gas + transaction_fee = tx_gas_used * priority_fee_per_gas + + # refund gas + create_ether(tx_state, sender, U256(gas_refund_amount)) + + # transfer miner fees + create_ether(tx_state, block_env.coinbase, U256(transaction_fee)) + + tx_state_gas = ( + int(tx_env.intrinsic_state_gas) + + tx_output.state_gas_used + - int(tx_output.state_refund) + ) + # Defensive guard for Uint conversion: State refunds never exceed + # the state charges so the value is non-negative. + tx_regular_gas = tx_gas_used_before_refund - Uint(max(0, tx_state_gas)) + block_output.block_gas_used += tx_regular_gas + block_output.block_state_gas_used += Uint(max(0, 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.cumulative_gas_used, tx_output.logs + ) + + receipt_key = rlp.encode(Uint(index)) + block_output.receipt_keys += (receipt_key,) + + trie_set( + block_output.receipts_trie, + receipt_key, + receipt, + ) + + block_output.block_logs += tx_output.logs + + for address in tx_output.accounts_to_delete: + clear_account_preserving_balance(tx_state, address) + + incorporate_tx_into_block(tx_state, block_env.block_access_list_builder) + + +def process_withdrawals( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + withdrawals: Tuple[Withdrawal, ...], +) -> None: + """ + Increase the balance of the withdrawing account. + """ + wd_state = TransactionState(parent=block_env.state) + + for i, wd in enumerate(withdrawals): + trie_set( + block_output.withdrawals_trie, + rlp.encode(Uint(i)), + rlp.encode(wd), + ) + + create_ether(wd_state, wd.address, wd.amount * GWEI_TO_WEI) + + incorporate_tx_into_block(wd_state, block_env.block_access_list_builder) + + +def check_gas_limit(gas_limit: Uint, parent_gas_limit: Uint) -> bool: + """ + Validates the gas limit for a block. + + The bounds of the gas limit, ``max_adjustment_delta``, is set as the + quotient of the parent block's gas limit and the + ``LIMIT_ADJUSTMENT_FACTOR``. Therefore, if the gas limit that is passed + through as a parameter is greater than or equal to the *sum* of the + parent's gas and the adjustment delta then the limit for gas is too high + and fails this function's check. Similarly, if the limit is less than or + equal to the *difference* of the parent's gas and the adjustment delta *or* + the predefined ``LIMIT_MINIMUM`` then this function's check fails because + the gas limit doesn't allow for a sufficient or reasonable amount of gas to + be used on a block. + + Parameters + ---------- + gas_limit : + Gas limit to validate. + + parent_gas_limit : + Gas limit of the parent block. + + Returns + ------- + check : `bool` + True if gas limit constraints are satisfied, False otherwise. + + """ + max_adjustment_delta = parent_gas_limit // GasCosts.LIMIT_ADJUSTMENT_FACTOR + if gas_limit >= parent_gas_limit + max_adjustment_delta: + return False + if gas_limit <= parent_gas_limit - max_adjustment_delta: + return False + if gas_limit < GasCosts.LIMIT_MINIMUM: + return False + + return True diff --git a/src/ethereum/forks/bogota/fork_types.py b/src/ethereum/forks/bogota/fork_types.py new file mode 100644 index 00000000000..78f4215cc12 --- /dev/null +++ b/src/ethereum/forks/bogota/fork_types.py @@ -0,0 +1,97 @@ +""" +Ethereum Types. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Types reused throughout the specification, which are specific to Ethereum. +""" + +from dataclasses import dataclass +from typing import NewType, final + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes256 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U8, U32, U64, U256, Uint + +from ethereum.crypto.hash import Hash32 +from ethereum.state import Account, Address + +BlockAccessIndex = U32 +""" +Position within the set of all changes in a [`Block`]. + +[`Block`]: ref:ethereum.forks.bogota.blocks.Block +""" + +VersionedHash = Hash32 + +Bloom = Bytes256 + + +RegularGas = NewType("RegularGas", Uint) + +StateGas = NewType("StateGas", Uint) + + +@final +@slotted_freezable +@dataclass +class StateGasPerByte: + """ + State gas charged per byte of state growth, per [EIP-8037]. + + A rate, not an amount: deliberately not a `Uint`, since adding a rate to + a gas amount is meaningless. Multiplying it by a byte count, in either + operand order, yields a `StateGas`. + + [EIP-8037]: https://eips.ethereum.org/EIPS/eip-8037 + """ + + rate: Uint + + def __mul__(self, num_bytes: Uint) -> StateGas: + """Return the state gas for `num_bytes` charged at this rate.""" + return StateGas(self.rate * num_bytes) + + def __rmul__(self, num_bytes: Uint) -> StateGas: + """Return the state gas for `num_bytes` charged at this rate.""" + return StateGas(self.rate * num_bytes) + + +def encode_account(raw_account_data: Account, storage_root: Bytes) -> Bytes: + """ + Encode `Account` dataclass. + + Storage is not stored in the `Account` dataclass, so `Accounts` cannot be + encoded without providing a storage root. + """ + return rlp.encode( + ( + raw_account_data.nonce, + raw_account_data.balance, + storage_root, + raw_account_data.code_hash, + ) + ) + + +@final +@slotted_freezable +@dataclass +class Authorization: + """ + The authorization for a set code transaction. + """ + + chain_id: U256 + address: Address + nonce: U64 + y_parity: U8 + r: U256 + s: U256 diff --git a/src/ethereum/forks/bogota/requests.py b/src/ethereum/forks/bogota/requests.py new file mode 100644 index 00000000000..f755d63427b --- /dev/null +++ b/src/ethereum/forks/bogota/requests.py @@ -0,0 +1,326 @@ +""" +[EIP-7685] generalizes how the execution layer communicates validator actions +to the consensus layer. Rather than adding a dedicated header field for each +new action type (as [EIP-4895] did for withdrawals), the execution header +commits to a single [`requests_hash`][rh] that aggregates an ordered list of +typed requests. + +Each request is a type byte (see [`DEPOSIT_REQUEST_TYPE`][dt], +[`WITHDRAWAL_REQUEST_TYPE`][wt], [`CONSOLIDATION_REQUEST_TYPE`][ct], +[`BUILDER_DEPOSIT_REQUEST_TYPE`][bd], and [`BUILDER_EXIT_REQUEST_TYPE`][be]) +followed by an opaque payload. Deposit requests are discovered by scanning +transaction receipts for logs emitted by the deposit contract; withdrawal, +consolidation, and builder deposit/exit requests ([EIP-8282]) are produced by +the corresponding system contracts during block processing. + +See [`parse_deposit_requests`][pd] for how deposit logs become request data, +[`compute_requests_hash`][crh] for how the list is hashed for inclusion in the +header, and [`process_general_purpose_requests`][pgpr] for how the requests are +processed. + +[EIP-4895]: https://eips.ethereum.org/EIPS/eip-4895 +[EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 +[EIP-8282]: https://eips.ethereum.org/EIPS/eip-8282 +[rh]: ref:ethereum.forks.bogota.blocks.Header.requests_hash +[dt]: ref:ethereum.forks.bogota.requests.DEPOSIT_REQUEST_TYPE +[wt]: ref:ethereum.forks.bogota.requests.WITHDRAWAL_REQUEST_TYPE +[ct]: ref:ethereum.forks.bogota.requests.CONSOLIDATION_REQUEST_TYPE +[bd]: ref:ethereum.forks.bogota.requests.BUILDER_DEPOSIT_REQUEST_TYPE +[be]: ref:ethereum.forks.bogota.requests.BUILDER_EXIT_REQUEST_TYPE +[pd]: ref:ethereum.forks.bogota.requests.parse_deposit_requests +[crh]: ref:ethereum.forks.bogota.requests.compute_requests_hash +[pgpr]: ref:ethereum.forks.bogota.fork.process_general_purpose_requests +""" + +from hashlib import sha256 +from typing import List + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import Uint, ulen + +from ethereum.exceptions import InvalidBlock +from ethereum.merkle_patricia_trie import trie_get +from ethereum.utils.hexadecimal import hex_to_bytes32 + +from .blocks import decode_receipt +from .utils.hexadecimal import hex_to_address +from .vm import BlockOutput + +DEPOSIT_CONTRACT_ADDRESS = hex_to_address( + "0x00000000219ab540356cbb839cbe05303d7705fa" +) +""" +Mainnet address of the beacon chain deposit contract. Scanning block +receipts for logs emitted by this address is how the execution layer +discovers validator deposits, per [EIP-6110]. + +[EIP-6110]: https://eips.ethereum.org/EIPS/eip-6110 +""" + +DEPOSIT_EVENT_SIGNATURE_HASH = hex_to_bytes32( + "0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5" +) +""" +First [log topic] of the deposit contract's `DepositEvent`, equal to the +keccak256 of its Solidity event signature. Logs whose first topic does not +match this are ignored when collecting deposit requests. + +[log topic]: https://docs.soliditylang.org/en/latest/abi-spec.html#events +""" + +DEPOSIT_REQUEST_TYPE = b"\x00" +""" +Request type byte identifying a deposit request, per [EIP-6110]. + +[EIP-6110]: https://eips.ethereum.org/EIPS/eip-6110 +""" + +WITHDRAWAL_REQUEST_TYPE = b"\x01" +""" +Request type byte identifying an execution-triggered withdrawal request, +per [EIP-7002]. + +[EIP-7002]: https://eips.ethereum.org/EIPS/eip-7002 +""" + +CONSOLIDATION_REQUEST_TYPE = b"\x02" +""" +Request type byte identifying a consolidation request, per [EIP-7251]. + +[EIP-7251]: https://eips.ethereum.org/EIPS/eip-7251 +""" + +BUILDER_DEPOSIT_REQUEST_TYPE = b"\x03" +""" +Request type byte identifying a builder deposit request, per [EIP-8282]. + +[EIP-8282]: https://eips.ethereum.org/EIPS/eip-8282 +""" + +BUILDER_EXIT_REQUEST_TYPE = b"\x04" +""" +Request type byte identifying a builder exit request, per [EIP-8282]. + +[EIP-8282]: https://eips.ethereum.org/EIPS/eip-8282 +""" + + +DEPOSIT_EVENT_LENGTH = Uint(576) +""" +Total length in bytes of the ABI-encoded `DepositEvent` data payload. Every +well-formed event has this exact length. +""" + +PUBKEY_OFFSET = Uint(160) +""" +Position within the event payload of the validator public key's length +prefix, as emitted by the Solidity ABI encoder. +""" + +WITHDRAWAL_CREDENTIALS_OFFSET = Uint(256) +""" +Position within the event payload of the withdrawal credentials' length +prefix. +""" + +AMOUNT_OFFSET = Uint(320) +""" +Position within the event payload of the deposit amount's length prefix. +""" + +SIGNATURE_OFFSET = Uint(384) +""" +Position within the event payload of the deposit signature's length prefix. +""" + +INDEX_OFFSET = Uint(512) +""" +Position within the event payload of the deposit index's length prefix. +""" + +PUBKEY_SIZE = Uint(48) +""" +Length of the BLS12-381 public key that identifies the validator receiving +the deposit. +""" + +WITHDRAWAL_CREDENTIALS_SIZE = Uint(32) +""" +Length of the withdrawal credentials, which determine where the staked +ether may eventually be withdrawn. +""" + +AMOUNT_SIZE = Uint(8) +""" +Length of the little-endian Gwei amount being deposited. +""" + +SIGNATURE_SIZE = Uint(96) +""" +Length of the BLS12-381 signature over the deposit message. +""" + +INDEX_SIZE = Uint(8) +""" +Length of the monotonically-increasing deposit index assigned by the +deposit contract when it emits the event. +""" + + +def extract_deposit_data(data: Bytes) -> Bytes: + """ + Strip the Solidity ABI framing from a `DepositEvent` payload and return + the concatenated raw fields in the order consumed by the consensus + layer: public key, withdrawal credentials, amount, signature, and + deposit index. + + Because each field has a fixed length, every well-formed event has an + identical byte layout. Any deviation indicates a misbehaving or + compromised deposit contract, so this function raises [`InvalidBlock`] + rather than silently accepting unexpected data. + + [`InvalidBlock`]: ref:ethereum.exceptions.InvalidBlock + """ + if ulen(data) != DEPOSIT_EVENT_LENGTH: + raise InvalidBlock("Invalid deposit event data length") + + # Check that all the offsets are in order + pubkey_offset = Uint.from_be_bytes(data[0:32]) + if pubkey_offset != PUBKEY_OFFSET: + raise InvalidBlock("Invalid pubkey offset in deposit log") + + withdrawal_credentials_offset = Uint.from_be_bytes(data[32:64]) + if withdrawal_credentials_offset != WITHDRAWAL_CREDENTIALS_OFFSET: + raise InvalidBlock( + "Invalid withdrawal credentials offset in deposit log" + ) + + amount_offset = Uint.from_be_bytes(data[64:96]) + if amount_offset != AMOUNT_OFFSET: + raise InvalidBlock("Invalid amount offset in deposit log") + + signature_offset = Uint.from_be_bytes(data[96:128]) + if signature_offset != SIGNATURE_OFFSET: + raise InvalidBlock("Invalid signature offset in deposit log") + + index_offset = Uint.from_be_bytes(data[128:160]) + if index_offset != INDEX_OFFSET: + raise InvalidBlock("Invalid index offset in deposit log") + + # Check that all the sizes are in order + pubkey_size = Uint.from_be_bytes( + data[pubkey_offset : pubkey_offset + Uint(32)] + ) + if pubkey_size != PUBKEY_SIZE: + raise InvalidBlock("Invalid pubkey size in deposit log") + + pubkey = data[ + pubkey_offset + Uint(32) : pubkey_offset + Uint(32) + PUBKEY_SIZE + ] + + withdrawal_credentials_size = Uint.from_be_bytes( + data[ + withdrawal_credentials_offset : withdrawal_credentials_offset + + Uint(32) + ], + ) + if withdrawal_credentials_size != WITHDRAWAL_CREDENTIALS_SIZE: + raise InvalidBlock( + "Invalid withdrawal credentials size in deposit log" + ) + + withdrawal_credentials = data[ + withdrawal_credentials_offset + + Uint(32) : withdrawal_credentials_offset + + Uint(32) + + WITHDRAWAL_CREDENTIALS_SIZE + ] + + amount_size = Uint.from_be_bytes( + data[amount_offset : amount_offset + Uint(32)] + ) + if amount_size != AMOUNT_SIZE: + raise InvalidBlock("Invalid amount size in deposit log") + + amount = data[ + amount_offset + Uint(32) : amount_offset + Uint(32) + AMOUNT_SIZE + ] + + signature_size = Uint.from_be_bytes( + data[signature_offset : signature_offset + Uint(32)] + ) + if signature_size != SIGNATURE_SIZE: + raise InvalidBlock("Invalid signature size in deposit log") + + signature = data[ + signature_offset + Uint(32) : signature_offset + + Uint(32) + + SIGNATURE_SIZE + ] + + index_size = Uint.from_be_bytes( + data[index_offset : index_offset + Uint(32)] + ) + if index_size != INDEX_SIZE: + raise InvalidBlock("Invalid index size in deposit log") + + index = data[ + index_offset + Uint(32) : index_offset + Uint(32) + INDEX_SIZE + ] + + return pubkey + withdrawal_credentials + amount + signature + index + + +def parse_deposit_requests(block_output: BlockOutput) -> Bytes: + """ + Walk the receipts produced during block execution, concatenating the + raw payload of every valid deposit event into a single byte string. + + A log is considered a deposit when it originates from + [`DEPOSIT_CONTRACT_ADDRESS`][addr] and its first topic matches + [`DEPOSIT_EVENT_SIGNATURE_HASH`][sig]. The returned bytes are the + direct concatenation of the unframed deposit fields, ready to be + prefixed with [`DEPOSIT_REQUEST_TYPE`][dt] before being appended to + the block's request list. + + [addr]: ref:ethereum.forks.bogota.requests.DEPOSIT_CONTRACT_ADDRESS + [sig]: ref:ethereum.forks.bogota.requests.DEPOSIT_EVENT_SIGNATURE_HASH + [dt]: ref:ethereum.forks.bogota.requests.DEPOSIT_REQUEST_TYPE + """ + deposit_requests: Bytes = b"" + for key in block_output.receipt_keys: + receipt = trie_get(block_output.receipts_trie, key) + assert receipt is not None + decoded_receipt = decode_receipt(receipt) + for log in decoded_receipt.logs: + if log.address == DEPOSIT_CONTRACT_ADDRESS: + if ( + len(log.topics) > 0 + and log.topics[0] == DEPOSIT_EVENT_SIGNATURE_HASH + ): + request = extract_deposit_data(log.data) + deposit_requests += request + + return deposit_requests + + +def compute_requests_hash(requests: List[Bytes]) -> Bytes: + """ + Compute the [SHA2-256] commitment over an ordered list of + type-prefixed requests, as defined by [EIP-7685]. + + The commitment is the SHA2-256 hash of the concatenation of the + SHA2-256 hashes of each individual request. This is what the + execution header's [`requests_hash`][rh] stores, and what the + consensus layer re-derives to validate that both layers observed the + same set of requests. + + [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 + [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 + [rh]: ref:ethereum.forks.bogota.blocks.Header.requests_hash + """ + m = sha256() + for request in requests: + m.update(sha256(request).digest()) + + return m.digest() diff --git a/src/ethereum/forks/bogota/state_tracker.py b/src/ethereum/forks/bogota/state_tracker.py new file mode 100644 index 00000000000..03ea6f33efa --- /dev/null +++ b/src/ethereum/forks/bogota/state_tracker.py @@ -0,0 +1,871 @@ +""" +State Tracking for Block Execution. + +Track state changes on top of a read-only ``PreState``. At block end, +accumulated diffs feed into +``PreState.compute_state_root_and_trie_changes()``. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Replace the mutable ``State`` class with lightweight state trackers that +record diffs. ``BlockState`` accumulates committed transaction +changes across a block. ``TransactionState`` tracks in-flight changes +within a single transaction and supports copy-on-write rollback. +""" + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Callable, Dict, Optional, Set, Tuple, final + +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.frozen import modify +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.state import ( + EMPTY_ACCOUNT, + EMPTY_CODE_HASH, + Account, + Address, + BlockDiff, + PreState, +) + +if TYPE_CHECKING: + from .block_access_lists import BlockAccessListBuilder + + +@final +@dataclass +class BlockState: + """ + Accumulate committed transaction-level changes across a block. + + Read chain: block writes -> pre_state. + + ``account_reads`` and ``storage_reads`` accumulate across all + transactions for BAL generation. + """ + + pre_state: PreState + account_reads: Set[Address] = field(default_factory=set) + account_writes: Dict[Address, Optional[Account]] = field( + default_factory=dict + ) + storage_reads: Set[Tuple[Address, Bytes32]] = field(default_factory=set) + storage_writes: Dict[Address, Dict[Bytes32, U256]] = field( + default_factory=dict + ) + code_writes: Dict[Hash32, Bytes] = field(default_factory=dict) + + +@final +@dataclass +class TransactionState: + """ + Track in-flight state changes within a single transaction. + + Read chain: tx writes -> block writes -> pre_state. + + ``storage_reads`` and ``account_reads`` are shared references + that survive rollback (reads from failed calls still appear in the + Block Access List). + """ + + parent: BlockState + account_reads: Set[Address] = field(default_factory=set) + account_writes: Dict[Address, Optional[Account]] = field( + default_factory=dict + ) + storage_reads: Set[Tuple[Address, Bytes32]] = field(default_factory=set) + storage_writes: Dict[Address, Dict[Bytes32, U256]] = field( + default_factory=dict + ) + code_writes: Dict[Hash32, Bytes] = field(default_factory=dict) + created_accounts: Set[Address] = field(default_factory=set) + transient_storage: Dict[Tuple[Address, Bytes32], U256] = field( + default_factory=dict + ) + + +def get_pre_state_account_optional( + tx_state: TransactionState, address: Address +) -> Optional[Account]: + """ + Get the `Account` object at an address that existed before the current + transaction, or `None` (rather than [`EMPTY_ACCOUNT`]) if there was no + account at the address at that point. + + Use [`get_pre_state_account()`][pre] if the difference between a + non-existent account and [`EMPTY_ACCOUNT`] isn't important. + + [`EMPTY_ACCOUNT`]: ref:ethereum.state.EMPTY_ACCOUNT + [pre]: ref:ethereum.forks.bogota.state_tracker.get_pre_state_account + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address to look up. + + Returns + ------- + account : ``Optional[Account]`` + Account at address before the current transaction. + + """ + tx_state.account_reads.add(address) + if address in tx_state.parent.account_writes: + return tx_state.parent.account_writes[address] + return tx_state.parent.pre_state.get_account_optional(address) + + +def get_pre_state_account( + tx_state: TransactionState, address: Address +) -> Account: + """ + Get the `Account` object at an address that existed before the current + transaction, or [`EMPTY_ACCOUNT`]) if there was no account at the address + at that point. + + Use [`get_pre_state_account_optional()`][opt] if the difference between a + non-existent account and [`EMPTY_ACCOUNT`] is material. + + [`EMPTY_ACCOUNT`]: ref:ethereum.state.EMPTY_ACCOUNT + [opt]: ref:ethereum.forks.bogota.state_tracker.get_pre_state_account_optional + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address to look up. + + Returns + ------- + account : ``Account`` + Account at address before the current transaction. + + """ # noqa: E501 + account = get_pre_state_account_optional(tx_state, address) + if account is None: + return EMPTY_ACCOUNT + else: + return account + + +def get_account_optional( + tx_state: TransactionState, address: Address +) -> Optional[Account]: + """ + Get the ``Account`` object at an address. Return ``None`` (rather than + ``EMPTY_ACCOUNT``) if there is no account at the address. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address to look up. + + Returns + ------- + account : ``Optional[Account]`` + Account at address. + + """ + tx_state.account_reads.add(address) + if address in tx_state.account_writes: + return tx_state.account_writes[address] + return get_pre_state_account_optional(tx_state, address) + + +def get_account(tx_state: TransactionState, address: Address) -> Account: + """ + Get the ``Account`` object at an address. Return ``EMPTY_ACCOUNT`` + if there is no account at the address. + + Use ``get_account_optional()`` if you care about the difference + between a non-existent account and ``EMPTY_ACCOUNT``. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address to look up. + + Returns + ------- + account : ``Account`` + Account at address. + + """ + account = get_account_optional(tx_state, address) + if account is None: + return EMPTY_ACCOUNT + else: + return account + + +def get_code(tx_state: TransactionState, code_hash: Hash32) -> Bytes: + """ + Get the bytecode for a given code hash. + + Read chain: tx code_writes -> block code_writes -> pre_state. + + Parameters + ---------- + tx_state : + The transaction state. + code_hash : + Hash of the code to look up. + + Returns + ------- + code : ``Bytes`` + The bytecode. + + """ + if code_hash == EMPTY_CODE_HASH: + return b"" + if code_hash in tx_state.code_writes: + return tx_state.code_writes[code_hash] + if code_hash in tx_state.parent.code_writes: + return tx_state.parent.code_writes[code_hash] + return tx_state.parent.pre_state.get_code(code_hash) + + +def get_storage( + tx_state: TransactionState, address: Address, key: Bytes32 +) -> U256: + """ + Get a value at a storage key on an account. Return ``U256(0)`` if + the storage key has not been set previously. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account. + key : + Key to look up. + + Returns + ------- + value : ``U256`` + Value at the key. + + """ + tx_state.storage_reads.add((address, key)) + if address in tx_state.storage_writes: + if key in tx_state.storage_writes[address]: + return tx_state.storage_writes[address][key] + if address in tx_state.parent.storage_writes: + if key in tx_state.parent.storage_writes[address]: + return tx_state.parent.storage_writes[address][key] + return tx_state.parent.pre_state.get_storage(address, key) + + +def get_storage_original( + tx_state: TransactionState, address: Address, key: Bytes32 +) -> U256: + """ + Get the original value in a storage slot i.e. the value before the + current transaction began. Read from block-level writes, then + pre_state. Return ``U256(0)`` for accounts created in the current + transaction. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account to read the value from. + key : + Key of the storage slot. + + """ + if address in tx_state.created_accounts: + return U256(0) + if address in tx_state.parent.storage_writes: + if key in tx_state.parent.storage_writes[address]: + return tx_state.parent.storage_writes[address][key] + return tx_state.parent.pre_state.get_storage(address, key) + + +def get_transient_storage( + tx_state: TransactionState, address: Address, key: Bytes32 +) -> U256: + """ + Get a value at a storage key on an account from transient storage. + Return ``U256(0)`` if the storage key has not been set previously. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account. + key : + Key to look up. + + Returns + ------- + value : ``U256`` + Value at the key. + + """ + return tx_state.transient_storage.get((address, key), U256(0)) + + +def account_exists(tx_state: TransactionState, address: Address) -> bool: + """ + Check if an account exists in the state trie. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account that needs to be checked. + + Returns + ------- + account_exists : ``bool`` + True if account exists in the state trie, False otherwise. + + """ + return get_account_optional(tx_state, address) is not None + + +def account_deployable(tx_state: TransactionState, address: Address) -> bool: + """ + Check if an account's code can be written to. + """ + account = get_account(tx_state, address) + if account.nonce != Uint(0) or account.code_hash != EMPTY_CODE_HASH: + return False + + if account_has_storage(tx_state, address): + return False + + return True + + +def account_has_storage(tx_state: TransactionState, address: Address) -> bool: + """ + Check if an account has storage. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account that needs to be checked. + + Returns + ------- + has_storage : ``bool`` + True if the account has storage, False otherwise. + + """ + if tx_state.storage_writes.get(address): + return True + if tx_state.parent.storage_writes.get(address): + return True + return tx_state.parent.pre_state.account_has_storage(address) + + +def account_exists_and_is_empty( + tx_state: TransactionState, address: Address +) -> bool: + """ + Check if an account exists and has zero nonce, empty code and zero + balance. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account that needs to be checked. + + Returns + ------- + exists_and_is_empty : ``bool`` + True if an account exists and has zero nonce, empty code and + zero balance, False otherwise. + + """ + account = get_account_optional(tx_state, address) + return ( + account is not None + and account.nonce == Uint(0) + and account.code_hash == EMPTY_CODE_HASH + and account.balance == 0 + ) + + +def is_account_alive(tx_state: TransactionState, address: Address) -> bool: + """ + Check whether an account is both in the state and non-empty. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account that needs to be checked. + + Returns + ------- + is_alive : ``bool`` + True if the account is alive. + + """ + account = get_account_optional(tx_state, address) + return account is not None and account != EMPTY_ACCOUNT + + +def set_account( + tx_state: TransactionState, + address: Address, + account: Optional[Account], +) -> None: + """ + Set the ``Account`` object at an address. Setting to ``None`` + deletes the account (but not its storage, see + ``destroy_account()``). + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address to set. + account : + Account to set at address. + + """ + tx_state.account_writes[address] = account + + +def set_storage( + tx_state: TransactionState, + address: Address, + key: Bytes32, + value: U256, +) -> None: + """ + Set a value at a storage key on an account. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account. + key : + Key to set. + value : + Value to set at the key. + + """ + assert get_account_optional(tx_state, address) is not None + if address not in tx_state.storage_writes: + tx_state.storage_writes[address] = {} + tx_state.storage_writes[address][key] = value + + +def destroy_account(tx_state: TransactionState, address: Address) -> None: + """ + Completely remove the account at ``address`` and all of its storage. + + Invoked by ``modify_state`` (and the coinbase fee-credit path) to + clean up an account that has become empty (zero nonce, empty + code, and zero balance) so it does not appear in the post-state. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of account to destroy. + + """ + destroy_storage(tx_state, address) + set_account(tx_state, address, None) + + +def clear_account_preserving_balance( + tx_state: TransactionState, address: Address +) -> None: + """ + Clear an account's nonce, code, and storage while preserving its + balance. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account to modify. + + """ + + def clear_account(account: Account) -> None: + account.nonce = Uint(0) + account.code_hash = EMPTY_CODE_HASH + + destroy_storage(tx_state, address) + modify_state(tx_state, address, clear_account) + + +def destroy_storage(tx_state: TransactionState, address: Address) -> None: + """ + Completely remove the storage at ``address``. + + Convert storage writes to reads before deleting so that accesses + from created-then-destroyed accounts appear in the Block Access + List. Only supports same transaction destruction. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of account whose storage is to be deleted. + + """ + if address in tx_state.storage_writes: + for key in tx_state.storage_writes[address]: + tx_state.storage_reads.add((address, key)) + del tx_state.storage_writes[address] + + +def mark_account_created(tx_state: TransactionState, address: Address) -> None: + """ + Mark an account as having been created in the current transaction. + This information is used by ``get_storage_original()`` to handle an + obscure edgecase, and to respect the constraints added to + SELFDESTRUCT by EIP-6780. + + The marker is not removed even if the account creation reverts. + Since the account cannot have had code prior to its creation and + can't call ``get_storage_original()``, this is harmless. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account that has been created. + + """ + tx_state.created_accounts.add(address) + + +def set_transient_storage( + tx_state: TransactionState, + address: Address, + key: Bytes32, + value: U256, +) -> None: + """ + Set a value at a storage key on an account in transient storage. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account. + key : + Key to set. + value : + Value to set at the key. + + """ + if value == U256(0): + tx_state.transient_storage.pop((address, key), None) + else: + tx_state.transient_storage[(address, key)] = value + + +def modify_state( + tx_state: TransactionState, + address: Address, + f: Callable[[Account], None], +) -> None: + """ + Modify an ``Account`` in the state. If, after modification, the + account exists and has zero nonce, empty code, and zero balance, it + is destroyed. + """ + set_account(tx_state, address, modify(get_account(tx_state, address), f)) + if account_exists_and_is_empty(tx_state, address): + destroy_account(tx_state, address) + + +def move_ether( + tx_state: TransactionState, + sender_address: Address, + recipient_address: Address, + amount: U256, +) -> None: + """ + Move funds between accounts. + + Parameters + ---------- + tx_state : + The transaction state. + sender_address : + Address of the sender. + recipient_address : + Address of the recipient. + amount : + The amount to transfer. + + """ + + def reduce_sender_balance(sender: Account) -> None: + if sender.balance < amount: + raise AssertionError + sender.balance -= amount + + def increase_recipient_balance(recipient: Account) -> None: + recipient.balance += amount + + modify_state(tx_state, sender_address, reduce_sender_balance) + modify_state(tx_state, recipient_address, increase_recipient_balance) + + +def create_ether( + tx_state: TransactionState, address: Address, amount: U256 +) -> None: + """ + Add newly created ether to an account. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account to which ether is added. + amount : + The amount of ether to be added to the account of interest. + + """ + + def increase_balance(account: Account) -> None: + account.balance += amount + + modify_state(tx_state, address, increase_balance) + + +def set_account_balance( + tx_state: TransactionState, address: Address, amount: U256 +) -> None: + """ + Set the balance of an account. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account whose balance needs to be set. + amount : + The amount that needs to be set in the balance. + + """ + + def set_balance(account: Account) -> None: + account.balance = amount + + modify_state(tx_state, address, set_balance) + + +def increment_nonce(tx_state: TransactionState, address: Address) -> None: + """ + Increment the nonce of an account. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account whose nonce needs to be incremented. + + """ + + def increase_nonce(sender: Account) -> None: + sender.nonce += Uint(1) + + modify_state(tx_state, address, increase_nonce) + + +def set_code( + tx_state: TransactionState, address: Address, code: Bytes +) -> None: + """ + Set Account code. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account whose code needs to be updated. + code : + The bytecode that needs to be set. + + """ + code_hash = keccak256(code) + if code_hash != EMPTY_CODE_HASH: + tx_state.code_writes[code_hash] = code + + def write_code_hash(sender: Account) -> None: + sender.code_hash = code_hash + + modify_state(tx_state, address, write_code_hash) + + +# -- Snapshot / Rollback --------------------------------------------------- + + +def copy_tx_state(tx_state: TransactionState) -> TransactionState: + """ + Create a snapshot of the transaction state for rollback. + + Deep-copy writes and transient storage. The parent reference, + ``created_accounts``, ``storage_reads``, and ``account_reads`` + are shared (not rolled back). + + Parameters + ---------- + tx_state : + The transaction state to snapshot. + + Returns + ------- + snapshot : ``TransactionState`` + A copy of the transaction state. + + """ + return TransactionState( + parent=tx_state.parent, + account_writes=dict(tx_state.account_writes), + storage_writes={ + addr: dict(slots) + for addr, slots in tx_state.storage_writes.items() + }, + code_writes=dict(tx_state.code_writes), + created_accounts=tx_state.created_accounts, + transient_storage=dict(tx_state.transient_storage), + storage_reads=tx_state.storage_reads, + account_reads=tx_state.account_reads, + ) + + +def restore_tx_state( + tx_state: TransactionState, snapshot: TransactionState +) -> None: + """ + Restore transaction state from a snapshot (rollback on failure). + + Parameters + ---------- + tx_state : + The transaction state to restore. + snapshot : + The snapshot to restore from. + + """ + tx_state.account_writes = snapshot.account_writes + tx_state.storage_writes = snapshot.storage_writes + tx_state.code_writes = snapshot.code_writes + tx_state.transient_storage = snapshot.transient_storage + + +# -- Lifecycle -------------------------------------------------------------- + + +def incorporate_tx_into_block( + tx_state: TransactionState, + builder: "BlockAccessListBuilder", +) -> None: + """ + Merge transaction writes into the block state and clear for reuse. + + Update the BAL builder incrementally by diffing this transaction's + writes against the block's cumulative state. Merge reads and + touches into block-level sets. + + Parameters + ---------- + tx_state : + The transaction state to commit. + builder : + The BAL builder for incremental updates. + + """ + from .block_access_lists import update_builder_from_tx + + block = tx_state.parent + + # Update BAL builder before merging writes into block state + update_builder_from_tx(builder, tx_state) + + # Merge reads and touches into block-level sets + block.storage_reads.update(tx_state.storage_reads) + block.account_reads.update(tx_state.account_reads) + + # Merge cumulative writes + for address, account in tx_state.account_writes.items(): + block.account_writes[address] = account + + for address, slots in tx_state.storage_writes.items(): + if address not in block.storage_writes: + block.storage_writes[address] = {} + block.storage_writes[address].update(slots) + + block.code_writes.update(tx_state.code_writes) + + tx_state.account_writes.clear() + tx_state.storage_writes.clear() + tx_state.code_writes.clear() + tx_state.created_accounts.clear() + tx_state.transient_storage.clear() + tx_state.storage_reads = set() + tx_state.account_reads = set() + + +def extract_block_diff(block_state: BlockState) -> BlockDiff: + """ + Extract account, storage, and code diff from the block state. + + Parameters + ---------- + block_state : + The block state. + + Returns + ------- + diff : `BlockDiff` + Account, storage, and code changes accumulated during block execution. + + """ + return BlockDiff( + account_changes=block_state.account_writes, + storage_changes=block_state.storage_writes, + code_changes=block_state.code_writes, + ) diff --git a/src/ethereum/forks/bogota/transactions.py b/src/ethereum/forks/bogota/transactions.py new file mode 100644 index 00000000000..9a0c76a007c --- /dev/null +++ b/src/ethereum/forks/bogota/transactions.py @@ -0,0 +1,1031 @@ +""" +Transactions are atomic units of work created externally to Ethereum and +submitted to be executed. If Ethereum is viewed as a state machine, +transactions are the events that move between states. +""" + +from dataclasses import dataclass +from typing import Tuple, TypeGuard, final + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes0, Bytes32 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U64, U256, Uint, ulen + +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import ( + InsufficientTransactionGasError, + InvalidSignatureError, + NonceOverflowError, +) +from ethereum.state import Address + +from .exceptions import ( + InitCodeTooLargeError, + TransactionTypeError, +) +from .fork_types import ( + Authorization, + RegularGas, + StateGas, + VersionedHash, +) + + +@final +@dataclass +class IntrinsicGasCost: + """Intrinsic gas costs for a transaction, split by gas type.""" + + regular: RegularGas + """Regular execution gas (calldata, base cost, access list, etc.).""" + + state: StateGas + """ + State growth gas (account creation, storage set, authorization) per + [EIP-8037]. + + [EIP-8037]: https://eips.ethereum.org/EIPS/eip-8037 + """ + + calldata_floor: RegularGas + """ + Minimum gas cost based on calldata size per [EIP-7623]. + + [EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 + """ + + +TX_MAX_GAS_LIMIT = Uint(16_777_216) + +ACCESS_LIST_ADDRESS_FLOOR_TOKENS = Uint(80) +""" +Floor data tokens contributed by a single access list address per +[EIP-7981]. + +[EIP-7981]: https://eips.ethereum.org/EIPS/eip-7981 +""" + +ACCESS_LIST_STORAGE_KEY_FLOOR_TOKENS = Uint(128) +""" +Floor data tokens contributed by a single access list storage key per +[EIP-7981]. + +[EIP-7981]: https://eips.ethereum.org/EIPS/eip-7981 +""" + + +@final +@slotted_freezable +@dataclass +class LegacyTransaction: + """ + Atomic operation performed on the block chain. This represents the original + transaction format used before [EIP-1559], [EIP-2930], [EIP-4844], + and [EIP-7702]. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 + """ + + nonce: U256 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + gas_price: Uint + """ + The price of gas for this transaction, in wei. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Bytes0 | Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + v: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +@final +@slotted_freezable +@dataclass +class Access: + """ + A mapping from account address to storage slots that are pre-warmed as part + of a transaction. + """ + + account: Address + """ + The address of the account that is accessed. + """ + + slots: Tuple[Bytes32, ...] + """ + A tuple of storage slots that are accessed in the account. + """ + + +@final +@slotted_freezable +@dataclass +class AccessListTransaction: + """ + The transaction type added in [EIP-2930] to support access lists. + + This transaction type extends the legacy transaction with an access list + and chain ID. The access list specifies which addresses and storage slots + the transaction will access. + + [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + """ + + chain_id: U64 + """ + The ID of the chain on which this transaction is executed. + """ + + nonce: U256 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + gas_price: Uint + """ + The price of gas for this transaction. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Bytes0 | Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + access_list: Tuple[Access, ...] + """ + A tuple of `Access` objects that specify which addresses and storage slots + are accessed in the transaction. + """ + + y_parity: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +@final +@slotted_freezable +@dataclass +class FeeMarketTransaction: + """ + The transaction type added in [EIP-1559]. + + This transaction type introduces a new fee market mechanism with two gas + price parameters: max_priority_fee_per_gas and max_fee_per_gas. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + """ + + chain_id: U64 + """ + The ID of the chain on which this transaction is executed. + """ + + nonce: U256 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + max_priority_fee_per_gas: Uint + """ + The maximum priority fee per gas that the sender is willing to pay. + """ + + max_fee_per_gas: Uint + """ + The maximum fee per gas that the sender is willing to pay, including the + base fee and priority fee. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Bytes0 | Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + access_list: Tuple[Access, ...] + """ + A tuple of `Access` objects that specify which addresses and storage slots + are accessed in the transaction. + """ + + y_parity: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +@final +@slotted_freezable +@dataclass +class BlobTransaction: + """ + The transaction type added in [EIP-4844]. + + This transaction type extends the fee market transaction to support + blob-carrying transactions. + + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + """ + + chain_id: U64 + """ + The ID of the chain on which this transaction is executed. + """ + + nonce: U256 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + max_priority_fee_per_gas: Uint + """ + The maximum priority fee per gas that the sender is willing to pay. + """ + + max_fee_per_gas: Uint + """ + The maximum fee per gas that the sender is willing to pay, including the + base fee and priority fee. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + access_list: Tuple[Access, ...] + """ + A tuple of `Access` objects that specify which addresses and storage slots + are accessed in the transaction. + """ + + max_fee_per_blob_gas: U256 + """ + The maximum fee per blob gas that the sender is willing to pay. + """ + + blob_versioned_hashes: Tuple[VersionedHash, ...] + """ + A tuple of objects that represent the versioned hashes of the blobs + included in the transaction. + """ + + y_parity: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +@final +@slotted_freezable +@dataclass +class SetCodeTransaction: + """ + The transaction type added in [EIP-7702]. + + This transaction type allows Ethereum Externally Owned Accounts (EOAs) + to set code on their account, enabling them to act as smart contracts. + + [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 + """ + + chain_id: U64 + """ + The ID of the chain on which this transaction is executed. + """ + + nonce: U64 + """ + A scalar value equal to the number of transactions sent by the sender. + """ + + max_priority_fee_per_gas: Uint + """ + The maximum priority fee per gas that the sender is willing to pay. + """ + + max_fee_per_gas: Uint + """ + The maximum fee per gas that the sender is willing to pay, including the + base fee and priority fee. + """ + + gas: Uint + """ + The maximum amount of gas that can be used by this transaction. + """ + + to: Address + """ + The address of the recipient. If empty, the transaction is a contract + creation. + """ + + value: U256 + """ + The amount of ether (in wei) to send with this transaction. + """ + + data: Bytes + """ + The data payload of the transaction, which can be used to call functions + on contracts or to create new contracts. + """ + + access_list: Tuple[Access, ...] + """ + A tuple of `Access` objects that specify which addresses and storage slots + are accessed in the transaction. + """ + + authorizations: Tuple[Authorization, ...] + """ + A tuple of `Authorization` objects that specify what code the signer + desires to execute in the context of their EOA. + """ + + y_parity: U256 + """ + The recovery id of the signature. + """ + + r: U256 + """ + The first part of the signature. + """ + + s: U256 + """ + The second part of the signature. + """ + + +Transaction = ( + LegacyTransaction + | AccessListTransaction + | FeeMarketTransaction + | BlobTransaction + | SetCodeTransaction +) +""" +Union type representing any valid transaction type. +""" + + +AccessListCapableTransaction = ( + AccessListTransaction + | FeeMarketTransaction + | BlobTransaction + | SetCodeTransaction +) +""" +Transaction types that include an [EIP-2930]-style access list. + +See [`has_access_list`][hal] and [`Access`][a] for more details. + +[EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 +[hal]: ref:ethereum.forks.bogota.transactions.has_access_list +[a]: ref:ethereum.forks.bogota.transactions.Access +""" + + +FeeMarketCapableTransaction = ( + FeeMarketTransaction | BlobTransaction | SetCodeTransaction +) +""" +Transaction types that include the [EIP-1559]-style fee structure. + +See [`FeeMarketTransaction`][fmt] for more details. + +[EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 +[fmt]: ref:ethereum.forks.bogota.transactions.FeeMarketTransaction +""" + + +def encode_transaction(tx: Transaction) -> LegacyTransaction | Bytes: + """ + Encode a transaction into its RLP or typed transaction format. + Needed because non-legacy transactions aren't RLP. + + Legacy transactions are returned as-is, while other transaction types + are prefixed with their type identifier and RLP encoded. + """ + if isinstance(tx, LegacyTransaction): + return tx + elif isinstance(tx, AccessListTransaction): + return b"\x01" + rlp.encode(tx) + elif isinstance(tx, FeeMarketTransaction): + return b"\x02" + rlp.encode(tx) + elif isinstance(tx, BlobTransaction): + return b"\x03" + rlp.encode(tx) + elif isinstance(tx, SetCodeTransaction): + return b"\x04" + rlp.encode(tx) + else: + raise Exception(f"Unable to encode transaction of type {type(tx)}") + + +def decode_transaction(tx: LegacyTransaction | Bytes) -> Transaction: + """ + Decode a transaction from its RLP or typed transaction format. + Needed because non-legacy transactions aren't RLP. + + Accept a ``LegacyTransaction`` object (returned as-is) or raw + bytes. + + EIP-2718 states that the first byte distinguishes the format: + [0x00, 0x7f] is a typed transaction, [0xc0, 0xfe] is a legacy + transaction (RLP list prefix). + """ + if isinstance(tx, Bytes): + if tx[0] == 1: + return rlp.decode_to(AccessListTransaction, tx[1:]) + elif tx[0] == 2: + return rlp.decode_to(FeeMarketTransaction, tx[1:]) + elif tx[0] == 3: + return rlp.decode_to(BlobTransaction, tx[1:]) + elif tx[0] == 4: + return rlp.decode_to(SetCodeTransaction, tx[1:]) + elif tx[0] >= 0xC0: + assert tx[0] <= 0xFE + return rlp.decode_to(LegacyTransaction, tx) + else: + raise TransactionTypeError(tx[0]) + else: + return tx + + +def validate_transaction(tx: Transaction, sender: Address) -> IntrinsicGasCost: + """ + Verifies a transaction. + + The gas in a transaction gets used to pay for the intrinsic cost of + operations, therefore if there is insufficient gas then it would not + be possible to execute a transaction and it will be declared invalid. + + Additionally, the nonce of a transaction must not equal or exceed the + limit defined in [EIP-2681]. + In practice, defining the limit as ``2**64-1`` has no impact because + sending ``2**64-1`` transactions is improbable. It's not strictly + impossible though, ``2**64-1`` transactions is the entire capacity of the + Ethereum blockchain at 2022 gas limits for a little over 22 years. + + Also, the code size of a contract creation transaction must be within + limits of the protocol. + + This function takes a transaction and gas_limit as parameters and + returns the intrinsic gas costs 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 overflows. + It also raises an `InitCodeTooLargeError` if the code + size of a contract creation transaction exceeds the maximum allowed + size. + + [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 = calculate_intrinsic_cost(tx, sender) + intrinsic_gas = Uint(intrinsic.regular) + Uint(intrinsic.state) + if intrinsic_gas > tx.gas: + raise InsufficientTransactionGasError("Insufficient intrinsic gas") + if intrinsic.calldata_floor > tx.gas: + raise InsufficientTransactionGasError("Insufficient calldata floor") + if tx.to == Bytes0(b"") and len(tx.data) > MAX_INIT_CODE_SIZE: + raise InitCodeTooLargeError("Code size too large") + if intrinsic.regular > TX_MAX_GAS_LIMIT: + raise InsufficientTransactionGasError( + "Intrinsic regular gas exceeds TX_MAX_GAS_LIMIT" + ) + if intrinsic.calldata_floor > TX_MAX_GAS_LIMIT: + raise InsufficientTransactionGasError( + "Intrinsic calldata floor exceeds TX_MAX_GAS_LIMIT" + ) + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise NonceOverflowError("Nonce too high") + + return intrinsic + + +def calculate_intrinsic_cost( + tx: Transaction, sender: Address +) -> IntrinsicGasCost: + """ + Calculates the gas that is charged before execution is started. + + The intrinsic cost of the transaction is charged before execution has + begun. Functions/operations in the EVM cost money to execute so this + intrinsic cost is for the operations that need to be paid for as part of + the transaction. Data transfer, for example, is part of this intrinsic + cost. It costs ether to send data over the wire and that ether is + accounted for in the intrinsic cost calculated in this function. This + intrinsic cost must be calculated and paid for before execution in order + for all operations to be implemented. + + The intrinsic cost includes: + 1. Sender cost (`TX_BASE`). + 2. Recipient cost (`COLD_ACCOUNT_ACCESS` for a non-self-transfer + call, or `CREATE_ACCESS` plus `NEW_ACCOUNT` state gas for a + contract creation). + 3. Value cost (`TRANSFER_LOG_COST`, plus `TX_VALUE_COST` for a + non-self-transfer call) when ``tx.value > 0``. + 4. Calldata cost (zero and non-zero bytes). + 5. Access list entries (if applicable). + 6. Authorizations (if applicable). + + Self-transfers (``sender == tx.to``) skip the recipient and value + charges. + + This function takes a transaction and gas_limit as parameters and + 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, + StateGasCosts, + init_code_cost, + ) + + tokens_in_calldata = count_tokens_in_data(tx.data) + + data_cost = tokens_in_calldata * GasCosts.TX_DATA_TOKEN_STANDARD + + is_create = tx.to == Bytes0(b"") + is_self_transfer = tx.to == sender + + recipient_regular_gas = Uint(0) + recipient_state_gas = Uint(0) + if is_create: + recipient_regular_gas = GasCosts.CREATE_ACCESS + init_code_cost( + ulen(tx.data) + ) + recipient_state_gas = StateGasCosts.NEW_ACCOUNT + if tx.value > U256(0): + recipient_regular_gas += GasCosts.TRANSFER_LOG_COST + elif not is_self_transfer: + recipient_regular_gas = GasCosts.COLD_ACCOUNT_ACCESS + if tx.value > U256(0): + recipient_regular_gas += ( + GasCosts.TRANSFER_LOG_COST + GasCosts.TX_VALUE_COST + ) + + access_list_cost = 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 += ( + ulen(access.slots) * GasCosts.TX_ACCESS_LIST_STORAGE_KEY + ) + tokens_in_access_list += ACCESS_LIST_ADDRESS_FLOOR_TOKENS + tokens_in_access_list += ( + ulen(access.slots) * ACCESS_LIST_STORAGE_KEY_FLOOR_TOKENS + ) + + # Data token floor cost for access list bytes. + access_list_cost += tokens_in_access_list * GasCosts.TX_DATA_TOKEN_FLOOR + + auth_regular_gas = Uint(0) + auth_state_gas = Uint(0) + if isinstance(tx, SetCodeTransaction): + auth_regular_gas = ( + GasCosts.ACCOUNT_WRITE + GasCosts.REGULAR_PER_AUTH_BASE_COST + ) * ulen(tx.authorizations) + auth_state_gas = ( + StateGasCosts.NEW_ACCOUNT + StateGasCosts.AUTH_BASE + ) * ulen(tx.authorizations) + + # EIP-7976 floor tokens: all calldata bytes count uniformly. + floor_tokens_in_calldata = ulen(tx.data) * GasCosts.TX_DATA_TOKEN_STANDARD + + # Total floor tokens. + 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 = ( + total_floor_tokens * GasCosts.TX_DATA_TOKEN_FLOOR + GasCosts.TX_BASE + ) + + intrinsic_regular_gas = ( + GasCosts.TX_BASE + + data_cost + + recipient_regular_gas + + access_list_cost + + auth_regular_gas + ) + + intrinsic_state_gas = recipient_state_gas + auth_state_gas + + return IntrinsicGasCost( + regular=RegularGas(intrinsic_regular_gas), + state=StateGas(intrinsic_state_gas), + calldata_floor=RegularGas(data_floor_gas_cost), + ) + + +def count_tokens_in_data(data: bytes) -> Uint: + """ + Count the data tokens in arbitrary input bytes. + + Zero bytes count as 1 token; non-zero bytes count as 4 tokens. + """ + num_zeros = Uint(data.count(0)) + num_non_zeros = ulen(data) - num_zeros + + return num_zeros + num_non_zeros * Uint(4) + + +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: + """ + Extracts the sender address from a transaction. + + The v, r, and s values are the three parts that make up the signature + of a transaction. In order to recover the sender of a transaction the two + components needed are the signature (``v``, ``r``, and ``s``) and the + signing hash of the transaction. The sender's public key can be obtained + with these two values and therefore the sender address can be retrieved. + + This function takes chain_id and a transaction as parameters and returns + the address of the sender of the transaction. It raises an + `InvalidSignatureError` if the signature values (r, s, v) are invalid. + """ + r, s = tx.r, tx.s + if U256(0) >= r or r >= SECP256K1N: + raise InvalidSignatureError("bad r") + if U256(0) >= s or s > SECP256K1N // U256(2): + raise InvalidSignatureError("bad s") + + if isinstance(tx, LegacyTransaction): + v = tx.v + if v == 27 or v == 28: + public_key = secp256k1_recover( + r, s, v - U256(27), signing_hash_pre155(tx) + ) + else: + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) + public_key = secp256k1_recover( + r, + s, + v, + signing_hash_155(tx, tx_chain_id), + ) + elif isinstance(tx, AccessListTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_2930(tx) + ) + elif isinstance(tx, FeeMarketTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_1559(tx) + ) + elif isinstance(tx, BlobTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_4844(tx) + ) + elif isinstance(tx, SetCodeTransaction): + if tx.y_parity not in (U256(0), U256(1)): + raise InvalidSignatureError("bad y_parity") + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_7702(tx) + ) + + return Address(keccak256(public_key)[12:32]) + + +def signing_hash_pre155(tx: LegacyTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a legacy (pre [EIP-155]) + signature. + + This function takes a legacy transaction as a parameter and returns the + signing hash of the transaction. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + return keccak256( + rlp.encode( + ( + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + ) + ) + ) + + +def signing_hash_155(tx: LegacyTransaction, chain_id: U64) -> Hash32: + """ + Compute the hash of a transaction used in a [EIP-155] signature. + + This function takes a legacy transaction and a chain ID as parameters + and returns the hash of the transaction used in an [EIP-155] signature. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + return keccak256( + rlp.encode( + ( + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + chain_id, + Uint(0), + Uint(0), + ) + ) + ) + + +def signing_hash_2930(tx: AccessListTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a [EIP-2930] signature. + + This function takes an access list transaction as a parameter + and returns the hash of the transaction used in an [EIP-2930] signature. + + [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + """ + return keccak256( + b"\x01" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.gas_price, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + ) + ) + ) + + +def signing_hash_1559(tx: FeeMarketTransaction) -> Hash32: + """ + Compute the hash of a transaction used in an [EIP-1559] signature. + + This function takes a fee market transaction as a parameter + and returns the hash of the transaction used in an [EIP-1559] signature. + + [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + """ + return keccak256( + b"\x02" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + ) + ) + ) + + +def signing_hash_4844(tx: BlobTransaction) -> Hash32: + """ + Compute the hash of a transaction used in an [EIP-4844] signature. + + This function takes a transaction as a parameter and returns the + signing hash of the transaction used in an [EIP-4844] signature. + + [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + """ + return keccak256( + b"\x03" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + tx.max_fee_per_blob_gas, + tx.blob_versioned_hashes, + ) + ) + ) + + +def signing_hash_7702(tx: SetCodeTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a [EIP-7702] signature. + + This function takes a transaction as a parameter and returns the + signing hash of the transaction used in a [EIP-7702] signature. + + [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 + """ + return keccak256( + b"\x04" + + rlp.encode( + ( + tx.chain_id, + tx.nonce, + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas, + tx.gas, + tx.to, + tx.value, + tx.data, + tx.access_list, + tx.authorizations, + ) + ) + ) + + +def get_transaction_hash(tx: Bytes | LegacyTransaction) -> Hash32: + """ + Compute the hash of a transaction. + + This function takes a transaction as a parameter and returns the + keccak256 hash of the transaction. It can handle both legacy transactions + and typed transactions (`AccessListTransaction`, `FeeMarketTransaction`, + etc.). + """ + assert isinstance(tx, (LegacyTransaction, Bytes)) + if isinstance(tx, LegacyTransaction): + return keccak256(rlp.encode(tx)) + else: + return keccak256(tx) + + +def has_access_list( + tx: Transaction, +) -> TypeGuard[AccessListCapableTransaction]: + """ + Return whether the transaction has an [EIP-2930]-style access list. + + [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + """ + return isinstance( + tx, + AccessListCapableTransaction, + ) diff --git a/src/ethereum/forks/bogota/utils/__init__.py b/src/ethereum/forks/bogota/utils/__init__.py new file mode 100644 index 00000000000..224a4d269b9 --- /dev/null +++ b/src/ethereum/forks/bogota/utils/__init__.py @@ -0,0 +1,3 @@ +""" +Utility functions unique to this particular fork. +""" diff --git a/src/ethereum/forks/bogota/utils/address.py b/src/ethereum/forks/bogota/utils/address.py new file mode 100644 index 00000000000..505dac6f1dc --- /dev/null +++ b/src/ethereum/forks/bogota/utils/address.py @@ -0,0 +1,93 @@ +""" +Hardfork Utility Functions For Addresses. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Address specific functions used in this bogota version of +specification. +""" + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.hash import keccak256 +from ethereum.state import Address +from ethereum.utils.byte import left_pad_zero_bytes + + +def to_address_masked(data: Uint | U256) -> Address: + """ + Convert a Uint or U256 value to a valid address (20 bytes). + + Parameters + ---------- + data : + The numeric value to be converted to address. + + Returns + ------- + address : `Address` + The obtained address. + + """ + return Address(data.to_be_bytes32()[-20:]) + + +def compute_contract_address(address: Address, nonce: Uint) -> Address: + """ + Computes address of the new account that needs to be created. + + Parameters + ---------- + address : + The address of the account that wants to create the new account. + nonce : + The transaction count of the account that wants to create the new + account. + + Returns + ------- + address: `Address` + The computed address of the new account. + + """ + computed_address = keccak256(rlp.encode([address, nonce])) + canonical_address = computed_address[-20:] + padded_address = left_pad_zero_bytes(canonical_address, 20) + return Address(padded_address) + + +def compute_create2_contract_address( + address: Address, salt: Bytes32, call_data: Bytes +) -> Address: + """ + Computes address of the new account that needs to be created, which is + based on the sender address, salt and the call data as well. + + Parameters + ---------- + address : + The address of the account that wants to create the new account. + salt : + Address generation salt. + call_data : + The code of the new account which is to be created. + + Returns + ------- + address: `ethereum.forks.bogota.fork_types.Address` + The computed address of the new account. + + """ + preimage = b"\xff" + address + salt + keccak256(call_data) + computed_address = keccak256(preimage) + canonical_address = computed_address[-20:] + padded_address = left_pad_zero_bytes(canonical_address, 20) + + return Address(padded_address) diff --git a/src/ethereum/forks/bogota/utils/hexadecimal.py b/src/ethereum/forks/bogota/utils/hexadecimal.py new file mode 100644 index 00000000000..e1aff1f2cd4 --- /dev/null +++ b/src/ethereum/forks/bogota/utils/hexadecimal.py @@ -0,0 +1,54 @@ +""" +Utility Functions For Hexadecimal Strings. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Hexadecimal utility functions used in this specification, specific to +Bogota types. +""" + +from ethereum_types.bytes import Bytes + +from ethereum.state import Address, Root +from ethereum.utils.hexadecimal import remove_hex_prefix + + +def hex_to_root(hex_string: str) -> Root: + """ + Convert hex string to trie root. + + Parameters + ---------- + hex_string : + The hexadecimal string to be converted to trie root. + + Returns + ------- + root : `Root` + Trie root obtained from the given hexadecimal string. + + """ + return Root(Bytes.fromhex(remove_hex_prefix(hex_string))) + + +def hex_to_address(hex_string: str) -> Address: + """ + Convert hex string to Address (20 bytes). + + Parameters + ---------- + hex_string : + The hexadecimal string to be converted to Address. + + Returns + ------- + address : `Address` + The address obtained from the given hexadecimal string. + + """ + return Address(Bytes.fromhex(remove_hex_prefix(hex_string).rjust(40, "0"))) diff --git a/src/ethereum/forks/bogota/utils/message.py b/src/ethereum/forks/bogota/utils/message.py new file mode 100644 index 00000000000..4cef9d19264 --- /dev/null +++ b/src/ethereum/forks/bogota/utils/message.py @@ -0,0 +1,94 @@ +""" +Hardfork Utility Functions For The Message Data-structure. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Message specific functions used in this bogota version of +specification. +""" + +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import Uint + +from ethereum.state import Address + +from ..state_tracker import get_account, get_code +from ..transactions import Transaction +from ..vm import BlockEnvironment, Message, TransactionEnvironment +from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS +from .address import compute_contract_address + + +def prepare_message( + block_env: BlockEnvironment, + tx_env: TransactionEnvironment, + tx: Transaction, +) -> Message: + """ + Execute a transaction against the provided environment. + + Parameters + ---------- + block_env : + Environment for the Ethereum Virtual Machine. + tx_env : + Environment for the transaction. + tx : + Transaction to be executed. + + Returns + ------- + message: `ethereum.forks.bogota.vm.Message` + Items containing contract creation or message call specific data. + + """ + accessed_addresses = set() + accessed_addresses.add(tx_env.origin) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_addresses.update(tx_env.access_list_addresses) + + if isinstance(tx.to, Bytes0): + current_target = compute_contract_address( + tx_env.origin, + get_account(tx_env.state, tx_env.origin).nonce - Uint(1), + ) + msg_data = Bytes(b"") + code = tx.data + code_address = None + elif isinstance(tx.to, Address): + current_target = tx.to + msg_data = tx.data + code = get_code( + tx_env.state, get_account(tx_env.state, tx.to).code_hash + ) + code_address = tx.to + else: + raise AssertionError("Target must be address or empty bytes") + + accessed_addresses.add(current_target) + + return Message( + block_env=block_env, + tx_env=tx_env, + 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, + depth=Uint(0), + current_target=current_target, + code_address=code_address, + should_transfer_value=True, + is_static=False, + accessed_addresses=accessed_addresses, + accessed_storage_keys=set(tx_env.access_list_storage_keys), + disable_precompiles=False, + parent_evm=None, + ) diff --git a/src/ethereum/forks/bogota/vm/__init__.py b/src/ethereum/forks/bogota/vm/__init__.py new file mode 100644 index 00000000000..27ea3398411 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/__init__.py @@ -0,0 +1,342 @@ +""" +Ethereum Virtual Machine (EVM). + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The abstract computer which runs the code stored in an +`.fork_types.Account`. +""" + +from dataclasses import dataclass, field +from typing import List, Optional, Set, Tuple, final + +from ethereum_types.bytes import Bytes, Bytes0, Bytes32 +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import EthereumException +from ethereum.merkle_patricia_trie import Trie +from ethereum.state import Address +from ethereum.utils.byte import left_pad_zero_bytes + +from ..block_access_lists import BlockAccessList, BlockAccessListBuilder +from ..blocks import Log, Receipt, Withdrawal +from ..fork_types import Authorization, StateGas, VersionedHash +from ..state_tracker import BlockState, TransactionState +from ..transactions import LegacyTransaction + +__all__ = ("Environment", "Evm", "Message") +TRANSFER_TOPIC = keccak256(b"Transfer(address,address,uint256)") +SYSTEM_ADDRESS = Address( + bytes.fromhex("fffffffffffffffffffffffffffffffffffffffe") +) +CALL_SUCCESS = U256(1) + + +@final +@dataclass +class BlockEnvironment: + """ + Items external to the virtual machine itself, provided by the environment. + """ + + chain_id: U64 + state: BlockState + block_gas_limit: Uint + block_hashes: List[Hash32] + coinbase: Address + number: Uint + base_fee_per_gas: Uint + time: U256 + prev_randao: Bytes32 + excess_blob_gas: U64 + parent_beacon_block_root: Hash32 + block_access_list_builder: BlockAccessListBuilder + slot_number: U64 + + +@final +@dataclass +class BlockOutput: + """ + Output from applying the block body to the present state. + + Contains the following: + + block_gas_used : `ethereum.base_types.Uint` + Gas used for executing all transactions. + block_state_gas_used : `ethereum.base_types.Uint` + State gas used for executing all transactions. + cumulative_gas_used : `ethereum.base_types.Uint` + Cumulative gas paid by users (post-refund, post-floor). + transactions_trie : `ethereum.fork_types.Root` + Trie of all the transactions in the block. + receipts_trie : `ethereum.fork_types.Root` + Trie root of all the receipts in the block. + receipt_keys : + Keys of all the receipts in the block. + block_logs : `Bloom` + Logs bloom of all the logs included in all the transactions of the + block. + withdrawals_trie : `ethereum.fork_types.Root` + Trie root of all the withdrawals in the block. + blob_gas_used : `ethereum.base_types.U64` + Total blob gas used in the block. + requests : `Bytes` + Hash of all the requests in the block. + block_access_list: `BlockAccessList` + The block access list for the block. + """ + + block_gas_used: Uint = Uint(0) + block_state_gas_used: Uint = Uint(0) + cumulative_gas_used: Uint = Uint(0) + transactions_trie: Trie[Bytes, Optional[Bytes | LegacyTransaction]] = ( + field(default_factory=lambda: Trie(secured=False, default=None)) + ) + receipts_trie: Trie[Bytes, Optional[Bytes | Receipt]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + receipt_keys: Tuple[Bytes, ...] = field(default_factory=tuple) + block_logs: Tuple[Log, ...] = field(default_factory=tuple) + withdrawals_trie: Trie[Bytes, Optional[Bytes | Withdrawal]] = field( + default_factory=lambda: Trie(secured=False, default=None) + ) + blob_gas_used: U64 = U64(0) + requests: List[Bytes] = field(default_factory=list) + block_access_list: BlockAccessList = field(default_factory=list) + + +@final +@dataclass +class TransactionEnvironment: + """ + Items that are used while processing a transaction. + """ + + origin: Address + recipient: Bytes0 | Address + value: U256 + gas_price: Uint + gas: Uint + state_gas_reservoir: Uint + access_list_addresses: Set[Address] + access_list_storage_keys: Set[Tuple[Address, Bytes32]] + state: TransactionState + blob_versioned_hashes: Tuple[VersionedHash, ...] + authorizations: Tuple[Authorization, ...] + index_in_block: Optional[Uint] + tx_hash: Optional[Hash32] + intrinsic_regular_gas: Uint + intrinsic_state_gas: Uint + + +@final +@dataclass +class Message: + """ + Items that are used by contract creation or message call. + """ + + block_env: BlockEnvironment + tx_env: TransactionEnvironment + caller: Address + target: Bytes0 | Address + current_target: Address + gas: Uint + state_gas_reservoir: Uint + value: U256 + data: Bytes + code_address: Optional[Address] + code: Bytes + depth: Uint + should_transfer_value: bool + is_static: bool + accessed_addresses: Set[Address] + accessed_storage_keys: Set[Tuple[Address, Bytes32]] + disable_precompiles: bool + parent_evm: Optional["Evm"] + + +@final +@dataclass +class Evm: + """The internal state of the virtual machine.""" + + pc: Uint + stack: List[U256] + memory: bytearray + code: Bytes + gas_left: Uint + state_gas_left: Uint + valid_jump_destinations: Set[Uint] + logs: Tuple[Log, ...] + refund_counter: int + running: bool + message: Message + output: Bytes + accounts_to_delete: Set[Address] + return_data: Bytes + error: Optional[EthereumException] + accessed_addresses: Set[Address] + accessed_storage_keys: Set[Tuple[Address, Bytes32]] + regular_gas_used: Uint = Uint(0) + state_gas_spilled: Uint = Uint(0) + + +def credit_state_gas_refund(evm: Evm, amount: StateGas) -> None: + """ + Credit a state gas refund to the local frame, in LIFO order. + + State-gas charges draw from the reservoir first and from `gas_left` + last, so refills credit the pool charged last first: `gas_left` up + to `state_gas_spilled`, then the reservoir. This restores the + exact pools the charge drew from, so the two never drift. + + Parameters + ---------- + evm : + The frame crediting the refund. + amount : + The refund amount to credit. + + """ + from_gas_left = min(amount, evm.state_gas_spilled) + evm.gas_left += from_gas_left + evm.state_gas_spilled -= from_gas_left + evm.state_gas_left += amount - from_gas_left + + +def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: + """ + Incorporate the state of a successful `child_evm` into the parent `evm`. + + Parameters + ---------- + evm : + The parent `EVM`. + child_evm : + The child evm to incorporate. + + """ + evm.gas_left += child_evm.gas_left + evm.state_gas_left += child_evm.state_gas_left + evm.state_gas_spilled += child_evm.state_gas_spilled + evm.logs += child_evm.logs + evm.refund_counter += child_evm.refund_counter + evm.accounts_to_delete.update(child_evm.accounts_to_delete) + evm.accessed_addresses.update(child_evm.accessed_addresses) + evm.accessed_storage_keys.update(child_evm.accessed_storage_keys) + evm.regular_gas_used += child_evm.regular_gas_used + + +def refill_frame_state_gas(evm: Evm) -> None: + """ + Roll back the frame's state gas in LIFO order on revert or halt. + + The frame's state changes are undone, so the state gas it consumed + is credited back to `gas_left` first and then to the reservoir, + restoring the pools the charges drew from. + + Parameters + ---------- + evm : + The frame whose state gas is rolled back. + + """ + evm.gas_left += evm.state_gas_spilled + evm.state_gas_left = evm.message.state_gas_reservoir + evm.state_gas_spilled = Uint(0) + + +def frame_state_gas_used(evm: Evm) -> int: + """ + Return the net state gas consumed by a finished frame. + + Equal to the reservoir drawn down ([`state_gas_reservoir`][sgr] at entry + minus the reservoir now) plus [`state_gas_spilled`][sgs]. May be negative + when refunds exceed charges. + + Parameters + ---------- + evm : + The finished frame. + + [sgr]: ref:ethereum.forks.bogota.vm.Message.state_gas_reservoir + [sgs]: ref:ethereum.forks.bogota.vm.Evm.state_gas_spilled + + """ + return ( + int(evm.message.state_gas_reservoir) + - int(evm.state_gas_left) + + int(evm.state_gas_spilled) + ) + + +def incorporate_child_on_error( + evm: Evm, + child_evm: Evm, +) -> None: + """ + Incorporate the state of an unsuccessful `child_evm` into the parent `evm`. + + The child rolls back its own state gas via `refill_frame_state_gas` + before returning (on both reverts and exceptional halts), so its + `gas_left` and reservoir already reflect the LIFO refill. The parent + therefore only reabsorbs the child's `gas_left` and reservoir. + + Parameters + ---------- + evm : + The parent `EVM`. + child_evm : + The child evm to incorporate. + + """ + evm.gas_left += child_evm.gas_left + evm.state_gas_left += child_evm.state_gas_left + evm.regular_gas_used += child_evm.regular_gas_used + + +def emit_transfer_log( + evm: Evm, + sender: Address, + recipient: Address, + transfer_amount: U256, +) -> None: + """ + Emit a LOG3 for all ETH transfers satisfying EIP-7708. + + Parameters + ---------- + evm : + The state of the ethereum virtual machine + sender : + The account address sending the transfer + recipient : + The account address receiving the transfer + transfer_amount : + The amount of ETH transacted + + """ + if transfer_amount == 0: + return + + padded_sender = left_pad_zero_bytes(sender, 32) + padded_recipient = left_pad_zero_bytes(recipient, 32) + log_entry = Log( + address=SYSTEM_ADDRESS, + topics=( + TRANSFER_TOPIC, + Hash32(padded_sender), + Hash32(padded_recipient), + ), + data=transfer_amount.to_be_bytes32(), + ) + + evm.logs = evm.logs + (log_entry,) diff --git a/src/ethereum/forks/bogota/vm/eoa_delegation.py b/src/ethereum/forks/bogota/vm/eoa_delegation.py new file mode 100644 index 00000000000..2060d5465d7 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/eoa_delegation.py @@ -0,0 +1,282 @@ +""" +Set EOA account code. +""" + +from typing import Optional, Tuple + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import keccak256 +from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ethereum.state import Address + +from ..fork_types import Authorization, StateGas +from ..state_tracker import ( + account_exists, + get_account, + get_code, + get_pre_state_account, + increment_nonce, + set_code, +) +from ..utils.hexadecimal import hex_to_address +from ..vm.gas import ( + GasCosts, + StateGasCosts, +) +from . import Evm, Message + +SET_CODE_TX_MAGIC = b"\x05" +EOA_DELEGATION_MARKER = b"\xef\x01\x00" +EOA_DELEGATION_MARKER_LENGTH = len(EOA_DELEGATION_MARKER) +EOA_DELEGATED_CODE_LENGTH = 23 +NULL_ADDRESS = hex_to_address("0x0000000000000000000000000000000000000000") + + +def is_valid_delegation(code: bytes) -> bool: + """ + Whether the code is a valid delegation designation. + + Parameters + ---------- + code: `bytes` + The code to check. + + Returns + ------- + valid : `bool` + True if the code is a valid delegation designation, + False otherwise. + + """ + return ( + len(code) == EOA_DELEGATED_CODE_LENGTH + and code[:EOA_DELEGATION_MARKER_LENGTH] == EOA_DELEGATION_MARKER + ) + + +def get_delegated_code_address(code: bytes) -> Optional[Address]: + """ + Get the address to which the code delegates. + + Parameters + ---------- + code: `bytes` + The code to get the address from. + + Returns + ------- + address : `Optional[Address]` + The address of the delegated code. + + """ + if is_valid_delegation(code): + return Address(code[EOA_DELEGATION_MARKER_LENGTH:]) + return None + + +def recover_authority(authorization: Authorization) -> Address: + """ + Recover the authority address from the authorization. + + Parameters + ---------- + authorization + The authorization to recover the authority from. + + Raises + ------ + InvalidSignatureError + If the signature is invalid. + + Returns + ------- + authority : `Address` + The recovered authority address. + + """ + y_parity, r, s = authorization.y_parity, authorization.r, authorization.s + if y_parity not in (0, 1): + raise InvalidSignatureError("Invalid y_parity in authorization") + if U256(0) >= r or r >= SECP256K1N: + raise InvalidSignatureError("Invalid r value in authorization") + if U256(0) >= s or s > SECP256K1N // U256(2): + raise InvalidSignatureError("Invalid s value in authorization") + + signing_hash = keccak256( + SET_CODE_TX_MAGIC + + rlp.encode( + ( + authorization.chain_id, + authorization.address, + authorization.nonce, + ) + ) + ) + + public_key = secp256k1_recover(r, s, U256(y_parity), signing_hash) + return Address(keccak256(public_key)[12:32]) + + +def calculate_delegation_cost( + evm: Evm, address: Address +) -> Tuple[bool, Address, Uint]: + """ + Get the delegation address and the cost of access from the address. + + Parameters + ---------- + evm : `Evm` + The execution frame. + address : `Address` + The address to check for delegation. + + Returns + ------- + delegation : `Tuple[bool, Address, Uint]` + The delegation address and access gas cost. + + """ + tx_state = evm.message.tx_env.state + + code = get_code(tx_state, get_account(tx_state, address).code_hash) + + if not is_valid_delegation(code): + return False, address, Uint(0) + + delegated_address = Address(code[EOA_DELEGATION_MARKER_LENGTH:]) + + if delegated_address in evm.accessed_addresses: + delegation_gas_cost = GasCosts.WARM_ACCESS + else: + delegation_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + + return True, delegated_address, delegation_gas_cost + + +def validate_authorization( + message: Message, auth: Authorization +) -> None | Tuple[Address, Bytes]: + """ + Check if the given `Authorization` is valid against the current state. + + Returns the `authority` address and its code, or `None` if the + validation was unsuccessful. + """ + tx_state = message.tx_env.state + + if auth.chain_id not in (message.block_env.chain_id, U256(0)): + return None + + if auth.nonce >= U64.MAX_VALUE: + return None + + try: + authority = recover_authority(auth) + except InvalidSignatureError: + return None + + message.accessed_addresses.add(authority) + + authority_account = get_account(tx_state, authority) + authority_code = get_code(tx_state, authority_account.code_hash) + + if authority_code and not is_valid_delegation(authority_code): + return None + + authority_nonce = authority_account.nonce + if authority_nonce != auth.nonce: + return None + + return (authority, authority_code) + + +def set_delegation(message: Message) -> Tuple[Uint, Uint]: + """ + Set the delegation code for the authorities in the message. + + Refills `StateGasCosts.NEW_ACCOUNT` when the authority's account + leaf already exists, and `StateGasCosts.AUTH_BASE` when its code + slot already holds a delegation indicator. When the authority leaf + already exists, the worst-case `GasCosts.ACCOUNT_WRITE` charged in + the intrinsic cost is also refunded to the regular-gas refund + counter. The totals are returned so block accounting can subtract + the state refill from `tx_state_gas` and apply the regular refund. + + Parameters + ---------- + message : + Transaction specific items. + + Returns + ------- + state_refund : `Uint` + Total state gas refunded across all processed authorizations. + regular_refund : `Uint` + Total regular gas (`ACCOUNT_WRITE`) refunded for authorities + whose account leaf already existed. + + """ + tx_state = message.tx_env.state + state_refund = Uint(0) + regular_refund = Uint(0) + for auth in message.tx_env.authorizations: + match validate_authorization(message, auth): + case None: + refund = StateGasCosts.AUTH_BASE + StateGasCosts.NEW_ACCOUNT + message.state_gas_reservoir += refund + state_refund += refund + regular_refund += GasCosts.ACCOUNT_WRITE + continue + case (authority, authority_code): + pass + + refund = StateGas(Uint(0)) + + if account_exists(tx_state, authority): + refund += StateGasCosts.NEW_ACCOUNT + # The new-account ACCOUNT_WRITE charged at intrinsic time is + # not needed: refund it to the regular refund counter. + regular_refund += GasCosts.ACCOUNT_WRITE + + pre_state_authority_account = get_pre_state_account( + tx_state, authority + ) + pre_state_authority_code = get_code( + tx_state, pre_state_authority_account.code_hash + ) + + delegated_before_tx = is_valid_delegation(pre_state_authority_code) + delegated_now = is_valid_delegation(authority_code) + + if auth.address == NULL_ADDRESS: + refund += StateGasCosts.AUTH_BASE + + if delegated_now and not delegated_before_tx: + refund += StateGasCosts.AUTH_BASE + + code_to_set = b"" + else: + code_to_set = EOA_DELEGATION_MARKER + auth.address + + if delegated_now or delegated_before_tx: + refund += StateGasCosts.AUTH_BASE + + set_code(tx_state, authority, code_to_set) + increment_nonce(tx_state, authority) + + message.state_gas_reservoir += refund + state_refund += refund + + if message.code_address is None: + raise InvalidBlock("Invalid type 4 transaction: no target") + + message.code = get_code( + tx_state, + get_account(tx_state, message.code_address).code_hash, + ) + + return state_refund, regular_refund diff --git a/src/ethereum/forks/bogota/vm/exceptions.py b/src/ethereum/forks/bogota/vm/exceptions.py new file mode 100644 index 00000000000..4bf3cee4055 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/exceptions.py @@ -0,0 +1,139 @@ +""" +Ethereum Virtual Machine (EVM) Exceptions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Exceptions which cause the EVM to halt exceptionally. +""" + +from ethereum.exceptions import EthereumException + + +class ExceptionalHalt(EthereumException): + """ + Indicates that the EVM has experienced an exceptional halt. This causes + execution to immediately end with all gas being consumed. + """ + + +class Revert(EthereumException): + """ + Raised by the `REVERT` opcode. + + Unlike other EVM exceptions this does not result in the consumption of all + gas. + """ + + pass + + +class StackUnderflowError(ExceptionalHalt): + """ + Occurs when a pop is executed on an empty stack. + """ + + pass + + +class StackOverflowError(ExceptionalHalt): + """ + Occurs when a push is executed on a stack at max capacity. + """ + + pass + + +class OutOfGasError(ExceptionalHalt): + """ + Occurs when an operation costs more than the amount of gas left in the + frame. + """ + + pass + + +class InvalidOpcode(ExceptionalHalt): + """ + Raised when an invalid opcode is encountered. + """ + + code: int + + def __init__(self, code: int) -> None: + super().__init__(code) + self.code = code + + +class InvalidJumpDestError(ExceptionalHalt): + """ + Occurs when the destination of a jump operation doesn't meet any of the + following criteria. + + * The jump destination is less than the length of the code. + * The jump destination should have the `JUMPDEST` opcode (0x5B). + * The jump destination shouldn't be part of the data corresponding to + `PUSH-N` opcodes. + """ + + +class StackDepthLimitError(ExceptionalHalt): + """ + Raised when the message depth is greater than `1024`. + """ + + pass + + +class WriteInStaticContext(ExceptionalHalt): + """ + Raised when an attempt is made to modify the state while operating inside + of a STATICCALL context. + """ + + pass + + +class OutOfBoundsRead(ExceptionalHalt): + """ + Raised when an attempt was made to read data beyond the + boundaries of the buffer. + """ + + pass + + +class InvalidParameter(ExceptionalHalt): + """ + Raised when invalid parameters are passed. + """ + + pass + + +class InvalidContractPrefix(ExceptionalHalt): + """ + Raised when the new contract code starts with 0xEF. + """ + + pass + + +class AddressCollision(ExceptionalHalt): + """ + Raised when the new contract address has a collision. + """ + + pass + + +class KZGProofError(ExceptionalHalt): + """ + Raised when the point evaluation precompile can't verify a proof. + """ + + pass diff --git a/src/ethereum/forks/bogota/vm/gas.py b/src/ethereum/forks/bogota/vm/gas.py new file mode 100644 index 00000000000..3924eb86e7f --- /dev/null +++ b/src/ethereum/forks/bogota/vm/gas.py @@ -0,0 +1,596 @@ +""" +Ethereum Virtual Machine (EVM) Gas. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +EVM gas constants and calculators. +""" + +from dataclasses import dataclass +from typing import Final, List, Tuple, final + +from ethereum_types.numeric import U64, U256, Uint, ulen + +from ethereum.forks.amsterdam.blocks import Header as PreviousHeader +from ethereum.trace import GasAndRefund, StateGasAndRefund, evm_trace +from ethereum.utils.numeric import ceil32, taylor_exponential + +from ..blocks import Header +from ..fork_types import StateGas, StateGasPerByte +from ..transactions import BlobTransaction, Transaction +from . import Evm +from .exceptions import OutOfGasError + + +# These may be patched at runtime by a future gas repricing utility to +# fast-iterate on state-byte costs. +class StateGasCosts: + """ + EIP-8037 state-gas constants. + + Kept separate from `GasCosts` because these carry a different unit: + state-byte counts that convert into gas via `COST_PER_STATE_BYTE`. + """ + + COST_PER_STATE_BYTE: Final[StateGasPerByte] = StateGasPerByte(Uint(1530)) + STATE_BYTES_PER_NEW_ACCOUNT: Final[Uint] = Uint(120) + STATE_BYTES_PER_STORAGE_SET: Final[Uint] = Uint(64) + STATE_BYTES_PER_AUTH_BASE: Final[Uint] = Uint(23) + STORAGE_SET: Final[StateGas] = ( + STATE_BYTES_PER_STORAGE_SET * COST_PER_STATE_BYTE + ) + NEW_ACCOUNT: Final[StateGas] = ( + STATE_BYTES_PER_NEW_ACCOUNT * COST_PER_STATE_BYTE + ) + AUTH_BASE: Final[StateGas] = ( + STATE_BYTES_PER_AUTH_BASE * COST_PER_STATE_BYTE + ) + + +# These values may be patched at runtime by a future gas repricing utility +class GasCosts: + """ + Constant gas values for the EVM. + """ + + # Tiers + BASE: Final[Uint] = Uint(2) + VERY_LOW: Final[Uint] = Uint(3) + LOW: Final[Uint] = Uint(5) + MID: Final[Uint] = Uint(8) + HIGH: Final[Uint] = Uint(10) + + # Access + WARM_ACCESS: Final[Uint] = Uint(100) + COLD_ACCOUNT_ACCESS: Final[Uint] = Uint(3000) + COLD_STORAGE_ACCESS: Final[Uint] = Uint(3000) + + # Storage + STORAGE_WRITE: Final[Uint] = Uint(10000) + + # Call + CALL_VALUE: Final[Uint] = Uint(10300) # ACCOUNT_WRITE + CALL_STIPEND + CALL_STIPEND: Final[Uint] = Uint(2300) + ACCOUNT_WRITE: Final[Uint] = Uint(8000) + + # Contract Creation + CODE_DEPOSIT_PER_BYTE: Final[Uint] = Uint(200) + CODE_INIT_PER_WORD: Final[Uint] = Uint(2) + CREATE_ACCESS: Final[Uint] = ACCOUNT_WRITE + COLD_STORAGE_ACCESS + + # Utility + ZERO: Final[Uint] = Uint(0) + MEMORY_PER_WORD: Final[Uint] = Uint(3) + FAST_STEP: Final[Uint] = Uint(5) + + # Refunds + REFUND_STORAGE_CLEAR: Final[int] = int( + (STORAGE_WRITE + COLD_STORAGE_ACCESS) * Uint(4800) // Uint(5000) + ) + + # Precompiles + PRECOMPILE_ECRECOVER: Final[Uint] = Uint(3000) + PRECOMPILE_P256VERIFY: Final[Uint] = Uint(6900) + PRECOMPILE_SHA256_BASE: Final[Uint] = Uint(60) + PRECOMPILE_SHA256_PER_WORD: Final[Uint] = Uint(12) + PRECOMPILE_RIPEMD160_BASE: Final[Uint] = Uint(600) + PRECOMPILE_RIPEMD160_PER_WORD: Final[Uint] = Uint(120) + PRECOMPILE_IDENTITY_BASE: Final[Uint] = Uint(15) + PRECOMPILE_IDENTITY_PER_WORD: Final[Uint] = Uint(3) + PRECOMPILE_BLAKE2F_PER_ROUND: Final[Uint] = Uint(1) + PRECOMPILE_POINT_EVALUATION: Final[Uint] = Uint(50000) + PRECOMPILE_BLS_G1ADD: Final[Uint] = Uint(375) + PRECOMPILE_BLS_G1MUL: Final[Uint] = Uint(12000) + PRECOMPILE_BLS_G1MAP: Final[Uint] = Uint(5500) + PRECOMPILE_BLS_G2ADD: Final[Uint] = Uint(600) + PRECOMPILE_BLS_G2MUL: Final[Uint] = Uint(22500) + PRECOMPILE_BLS_G2MAP: Final[Uint] = Uint(23800) + PRECOMPILE_ECADD: Final[Uint] = Uint(150) + PRECOMPILE_ECMUL: Final[Uint] = Uint(6000) + PRECOMPILE_ECPAIRING_BASE: Final[Uint] = Uint(45000) + PRECOMPILE_ECPAIRING_PER_POINT: Final[Uint] = Uint(34000) + + # Blobs + PER_BLOB: Final[U64] = U64(2**17) + BLOB_SCHEDULE_TARGET: Final[U64] = U64(14) + BLOB_TARGET_GAS_PER_BLOCK: Final[U64] = PER_BLOB * BLOB_SCHEDULE_TARGET + BLOB_BASE_COST: Final[Uint] = Uint(2**13) + BLOB_SCHEDULE_MAX: Final[U64] = U64(21) + BLOB_MIN_GASPRICE: Final[Uint] = Uint(1) + BLOB_BASE_FEE_UPDATE_FRACTION: Final[Uint] = Uint(11684671) + + # Block Access Lists + BLOCK_ACCESS_LIST_ITEM: Final[Uint] = Uint(2000) + + # Transactions + TX_BASE: Final[Uint] = Uint(12000) + TX_CREATE: Final[Uint] = Uint(32000) + TX_VALUE_COST: Final[Uint] = Uint(4244) + TRANSFER_LOG_COST: Final[Uint] = Uint(1756) + TX_DATA_TOKEN_STANDARD: Final[Uint] = Uint(4) + TX_DATA_TOKEN_FLOOR: Final[Uint] = Uint(16) + TX_ACCESS_LIST_ADDRESS: Final[Uint] = COLD_ACCOUNT_ACCESS + TX_ACCESS_LIST_STORAGE_KEY: Final[Uint] = COLD_STORAGE_ACCESS + + # Authorization + AUTH_TUPLE_BYTES: Final[Uint] = Uint(101) + REGULAR_PER_AUTH_BASE_COST: Final[Uint] = ( + AUTH_TUPLE_BYTES * TX_DATA_TOKEN_FLOOR + + PRECOMPILE_ECRECOVER + + COLD_ACCOUNT_ACCESS + + Uint(2) * WARM_ACCESS + ) + + # Block + LIMIT_ADJUSTMENT_FACTOR: Final[Uint] = Uint(1024) + LIMIT_MINIMUM: Final[Uint] = Uint(5000) + + # Static Opcodes + OPCODE_ADD: Final[Uint] = VERY_LOW + OPCODE_SUB: Final[Uint] = VERY_LOW + OPCODE_MUL: Final[Uint] = LOW + OPCODE_DIV: Final[Uint] = LOW + OPCODE_SDIV: Final[Uint] = LOW + OPCODE_MOD: Final[Uint] = LOW + OPCODE_SMOD: Final[Uint] = LOW + OPCODE_ADDMOD: Final[Uint] = MID + OPCODE_MULMOD: Final[Uint] = MID + OPCODE_SIGNEXTEND: Final[Uint] = LOW + OPCODE_LT: Final[Uint] = VERY_LOW + OPCODE_GT: Final[Uint] = VERY_LOW + OPCODE_SLT: Final[Uint] = VERY_LOW + OPCODE_SGT: Final[Uint] = VERY_LOW + OPCODE_EQ: Final[Uint] = VERY_LOW + OPCODE_ISZERO: Final[Uint] = VERY_LOW + OPCODE_AND: Final[Uint] = VERY_LOW + OPCODE_OR: Final[Uint] = VERY_LOW + OPCODE_XOR: Final[Uint] = VERY_LOW + OPCODE_NOT: Final[Uint] = VERY_LOW + OPCODE_BYTE: Final[Uint] = VERY_LOW + OPCODE_SHL: Final[Uint] = VERY_LOW + OPCODE_SHR: Final[Uint] = VERY_LOW + OPCODE_SAR: Final[Uint] = VERY_LOW + OPCODE_CLZ: Final[Uint] = LOW + OPCODE_JUMP: Final[Uint] = MID + OPCODE_JUMPI: Final[Uint] = HIGH + OPCODE_JUMPDEST: Final[Uint] = Uint(1) + OPCODE_CALLDATALOAD: Final[Uint] = VERY_LOW + OPCODE_BLOCKHASH: Final[Uint] = Uint(20) + OPCODE_COINBASE: Final[Uint] = BASE + OPCODE_POP: Final[Uint] = BASE + OPCODE_MSIZE: Final[Uint] = BASE + OPCODE_PC: Final[Uint] = BASE + OPCODE_GAS: Final[Uint] = BASE + OPCODE_ADDRESS: Final[Uint] = BASE + OPCODE_ORIGIN: Final[Uint] = BASE + OPCODE_CALLER: Final[Uint] = BASE + OPCODE_CALLVALUE: Final[Uint] = BASE + OPCODE_CALLDATASIZE: Final[Uint] = BASE + OPCODE_CODESIZE: Final[Uint] = BASE + OPCODE_GASPRICE: Final[Uint] = BASE + OPCODE_TIMESTAMP: Final[Uint] = BASE + OPCODE_NUMBER: Final[Uint] = BASE + OPCODE_GASLIMIT: Final[Uint] = BASE + OPCODE_PREVRANDAO: Final[Uint] = BASE + OPCODE_RETURNDATASIZE: Final[Uint] = BASE + OPCODE_CHAINID: Final[Uint] = BASE + OPCODE_BASEFEE: Final[Uint] = BASE + OPCODE_BLOBBASEFEE: Final[Uint] = BASE + OPCODE_SLOTNUM: Final[Uint] = BASE + OPCODE_BLOBHASH: Final[Uint] = Uint(3) + OPCODE_PUSH: Final[Uint] = VERY_LOW + OPCODE_PUSH0: Final[Uint] = BASE + OPCODE_DUP: Final[Uint] = VERY_LOW + OPCODE_SWAP: Final[Uint] = VERY_LOW + OPCODE_DUPN: Final[Uint] = VERY_LOW + OPCODE_SWAPN: Final[Uint] = VERY_LOW + OPCODE_EXCHANGE: Final[Uint] = VERY_LOW + OPCODE_TLOAD: Final[Uint] = Uint(100) + OPCODE_TSTORE: Final[Uint] = Uint(100) + + # Dynamic Opcode Components + OPCODE_RETURNDATACOPY_BASE: Final[Uint] = VERY_LOW + OPCODE_RETURNDATACOPY_PER_WORD: Final[Uint] = Uint(3) + OPCODE_CALLDATACOPY_BASE: Final[Uint] = VERY_LOW + OPCODE_CODECOPY_BASE: Final[Uint] = VERY_LOW + OPCODE_MCOPY_BASE: Final[Uint] = VERY_LOW + OPCODE_MLOAD_BASE: Final[Uint] = VERY_LOW + OPCODE_MSTORE_BASE: Final[Uint] = VERY_LOW + OPCODE_MSTORE8_BASE: Final[Uint] = VERY_LOW + OPCODE_COPY_PER_WORD: Final[Uint] = Uint(3) + OPCODE_EXP_BASE: Final[Uint] = Uint(10) + OPCODE_EXP_PER_BYTE: Final[Uint] = Uint(50) + OPCODE_KECCAK256_BASE: Final[Uint] = Uint(30) + OPCODE_KECCAK256_PER_WORD: Final[Uint] = Uint(6) + OPCODE_LOG_BASE: Final[Uint] = Uint(375) + OPCODE_LOG_DATA_PER_BYTE: Final[Uint] = Uint(8) + OPCODE_LOG_TOPIC: Final[Uint] = Uint(375) + OPCODE_SELFDESTRUCT_BASE: Final[Uint] = Uint(5000) + + +@final +@dataclass +class ExtendMemory: + """ + Define the parameters for memory extension in opcodes. + + `cost`: `ethereum.base_types.Uint` + The gas required to perform the extension + `expand_by`: `ethereum.base_types.Uint` + The size by which the memory will be extended + """ + + cost: Uint + expand_by: Uint + + +@final +@dataclass +class MessageCallGas: + """ + Define the gas cost and gas given to the sub-call for executing the call + opcodes. + + `cost`: `ethereum.base_types.Uint` + The gas required to execute the call opcode, excludes + memory expansion costs. + `sub_call`: `ethereum.base_types.Uint` + The portion of gas available to sub-calls that is refundable + if not consumed. + """ + + cost: Uint + sub_call: Uint + + +def check_gas(evm: Evm, amount: Uint) -> None: + """ + Checks if `amount` gas is available without charging it. + Raises OutOfGasError if insufficient gas. + + Parameters + ---------- + evm : + The current EVM. + amount : + The amount of gas to check. + + """ + if evm.gas_left < amount: + raise OutOfGasError + + +def charge_gas(evm: Evm, amount: Uint) -> None: + """ + Subtracts `amount` from `evm.gas_left` (regular gas) and records usage. + + Parameters + ---------- + evm : + The current EVM. + amount : + The amount of regular gas the current operation requires. + + """ + evm_trace(evm, GasAndRefund(int(amount))) + + if evm.gas_left < amount: + raise OutOfGasError + evm.gas_left -= amount + + evm.regular_gas_used += amount + + +def charge_state_gas(evm: Evm, amount: StateGas) -> None: + """ + Subtracts `amount` from the state gas reservoir, then from + `evm.gas_left` when the reservoir is empty, tracking any [spill]. + + Parameters + ---------- + evm : + The current EVM. + amount : + The amount of state gas the current operation requires. + + [spill]: ref:ethereum.forks.bogota.vm.Evm.state_gas_spilled + + """ + evm_trace(evm, StateGasAndRefund(int(amount))) + + if evm.state_gas_left >= amount: + evm.state_gas_left -= amount + elif evm.state_gas_left + evm.gas_left >= amount: + remainder = amount - evm.state_gas_left + evm.state_gas_left = Uint(0) + evm.gas_left -= remainder + evm.state_gas_spilled += remainder + else: + raise OutOfGasError + + +def calculate_memory_gas_cost(size_in_bytes: Uint) -> Uint: + """ + Calculates the gas cost for allocating memory + to the smallest multiple of 32 bytes, + such that the allocated size is at least as big as the given size. + + Parameters + ---------- + size_in_bytes : + The size of the data in bytes. + + Returns + ------- + total_gas_cost : `ethereum.base_types.Uint` + The gas cost for storing data in memory. + + """ + size_in_words = ceil32(size_in_bytes) // Uint(32) + linear_cost = size_in_words * GasCosts.MEMORY_PER_WORD + quadratic_cost = size_in_words ** Uint(2) // Uint(512) + total_gas_cost = linear_cost + quadratic_cost + try: + return total_gas_cost + except ValueError as e: + raise OutOfGasError from e + + +def calculate_gas_extend_memory( + memory: bytearray, extensions: List[Tuple[U256, U256]] +) -> ExtendMemory: + """ + Calculates the gas amount to extend memory. + + Parameters + ---------- + memory : + Memory contents of the EVM. + extensions: + List of extensions to be made to the memory. + Consists of a tuple of start position and size. + + Returns + ------- + extend_memory: `ExtendMemory` + + """ + size_to_extend = Uint(0) + to_be_paid = Uint(0) + current_size = ulen(memory) + for start_position, size in extensions: + if size == 0: + continue + before_size = ceil32(current_size) + after_size = ceil32(Uint(start_position) + Uint(size)) + if after_size <= before_size: + continue + + size_to_extend += after_size - before_size + already_paid = calculate_memory_gas_cost(before_size) + total_cost = calculate_memory_gas_cost(after_size) + to_be_paid += total_cost - already_paid + + current_size = after_size + + return ExtendMemory(to_be_paid, size_to_extend) + + +def calculate_message_call_gas( + value: U256, + gas: Uint, + gas_left: Uint, + memory_cost: Uint, + extra_gas: Uint, + call_stipend: Uint = GasCosts.CALL_STIPEND, +) -> MessageCallGas: + """ + Calculates the MessageCallGas (cost and gas made available to the sub-call) + for executing call Opcodes. + + Parameters + ---------- + value: + The amount of `ETH` that needs to be transferred. + gas : + The amount of gas provided to the message-call. + gas_left : + The amount of gas left in the current frame. + memory_cost : + The amount needed to extend the memory in the current frame. + extra_gas : + The amount of gas needed for transferring value + creating a new + account inside a message call. + call_stipend : + The amount of stipend provided to a message call to execute code while + transferring value (ETH). + + Returns + ------- + message_call_gas: `MessageCallGas` + + """ + call_stipend = Uint(0) if value == 0 else call_stipend + if gas_left < extra_gas + memory_cost: + return MessageCallGas(gas + extra_gas, gas + call_stipend) + + gas = min(gas, max_message_call_gas(gas_left - memory_cost - extra_gas)) + + return MessageCallGas(gas + extra_gas, gas + call_stipend) + + +def max_message_call_gas(gas: Uint) -> Uint: + """ + Calculates the maximum gas that is allowed for making a message call. + + Parameters + ---------- + gas : + The amount of gas provided to the message-call. + + Returns + ------- + max_allowed_message_call_gas: `ethereum.base_types.Uint` + The maximum gas allowed for making the message-call. + + """ + return gas - (gas // Uint(64)) + + +def init_code_cost(init_code_length: Uint) -> Uint: + """ + Calculates the gas to be charged for the init code in CREATE* + opcodes as well as create transactions. + + Parameters + ---------- + init_code_length : + The length of the init code provided to the opcode + or a create transaction + + Returns + ------- + init_code_gas: `ethereum.base_types.Uint` + The gas to be charged for the init code. + + """ + return GasCosts.CODE_INIT_PER_WORD * ceil32(init_code_length) // Uint(32) + + +def calculate_excess_blob_gas( + parent_header: Header | PreviousHeader, +) -> U64: + """ + Calculates the excess blob gas for the current block based + on the gas used in the parent block. + + Parameters + ---------- + parent_header : + The parent block of the current block. + + Returns + ------- + excess_blob_gas: `ethereum.base_types.U64` + The excess blob gas for the current block. + + """ + # At the fork block, these are defined as zero. + excess_blob_gas = U64(0) + blob_gas_used = U64(0) + base_fee_per_gas = Uint(0) + + if isinstance(parent_header, Header): + # After the fork block, read them from the parent header. + excess_blob_gas = parent_header.excess_blob_gas + blob_gas_used = parent_header.blob_gas_used + base_fee_per_gas = parent_header.base_fee_per_gas + + parent_blob_gas = excess_blob_gas + blob_gas_used + if parent_blob_gas < GasCosts.BLOB_TARGET_GAS_PER_BLOCK: + return U64(0) + + target_blob_gas_price = Uint(GasCosts.PER_BLOB) + target_blob_gas_price *= calculate_blob_gas_price(excess_blob_gas) + + base_blob_tx_price = GasCosts.BLOB_BASE_COST * base_fee_per_gas + if base_blob_tx_price > target_blob_gas_price: + blob_schedule_delta = ( + GasCosts.BLOB_SCHEDULE_MAX - GasCosts.BLOB_SCHEDULE_TARGET + ) + return ( + excess_blob_gas + + blob_gas_used * blob_schedule_delta // GasCosts.BLOB_SCHEDULE_MAX + ) + + return parent_blob_gas - GasCosts.BLOB_TARGET_GAS_PER_BLOCK + + +def calculate_total_blob_gas(tx: Transaction) -> U64: + """ + Calculate the total blob gas for a transaction. + + Parameters + ---------- + tx : + The transaction for which the blob gas is to be calculated. + + Returns + ------- + total_blob_gas: `ethereum.base_types.Uint` + The total blob gas for the transaction. + + """ + if isinstance(tx, BlobTransaction): + return GasCosts.PER_BLOB * U64(len(tx.blob_versioned_hashes)) + else: + return U64(0) + + +def calculate_blob_gas_price(excess_blob_gas: U64) -> Uint: + """ + Calculate the blob gasprice for a block. + + Parameters + ---------- + excess_blob_gas : + The excess blob gas for the block. + + Returns + ------- + blob_gasprice: `Uint` + The blob gasprice. + + """ + return taylor_exponential( + GasCosts.BLOB_MIN_GASPRICE, + Uint(excess_blob_gas), + GasCosts.BLOB_BASE_FEE_UPDATE_FRACTION, + ) + + +def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: + """ + Calculate the blob data fee for a transaction. + + Parameters + ---------- + excess_blob_gas : + The excess_blob_gas for the execution. + tx : + The transaction for which the blob data fee is to be calculated. + + Returns + ------- + data_fee: `Uint` + The blob data fee. + + """ + return Uint(calculate_total_blob_gas(tx)) * calculate_blob_gas_price( + excess_blob_gas + ) diff --git a/src/ethereum/forks/bogota/vm/instructions/__init__.py b/src/ethereum/forks/bogota/vm/instructions/__init__.py new file mode 100644 index 00000000000..06295ec86f1 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/instructions/__init__.py @@ -0,0 +1,377 @@ +""" +EVM Instruction Encoding (Opcodes). + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Machine readable representations of EVM instructions, and a mapping to their +implementations. +""" + +import enum +from typing import Callable, Dict + +from . import arithmetic as arithmetic_instructions +from . import bitwise as bitwise_instructions +from . import block as block_instructions +from . import comparison as comparison_instructions +from . import control_flow as control_flow_instructions +from . import environment as environment_instructions +from . import keccak as keccak_instructions +from . import log as log_instructions +from . import memory as memory_instructions +from . import stack as stack_instructions +from . import storage as storage_instructions +from . import system as system_instructions + + +class Ops(enum.Enum): + """ + Enum for EVM Opcodes. + """ + + # Arithmetic Ops + ADD = 0x01 + MUL = 0x02 + SUB = 0x03 + DIV = 0x04 + SDIV = 0x05 + MOD = 0x06 + SMOD = 0x07 + ADDMOD = 0x08 + MULMOD = 0x09 + EXP = 0x0A + SIGNEXTEND = 0x0B + + # Comparison Ops + LT = 0x10 + GT = 0x11 + SLT = 0x12 + SGT = 0x13 + EQ = 0x14 + ISZERO = 0x15 + + # Bitwise Ops + AND = 0x16 + OR = 0x17 + XOR = 0x18 + NOT = 0x19 + BYTE = 0x1A + SHL = 0x1B + SHR = 0x1C + SAR = 0x1D + CLZ = 0x1E + + # Keccak Op + KECCAK = 0x20 + + # Environmental Ops + ADDRESS = 0x30 + BALANCE = 0x31 + ORIGIN = 0x32 + CALLER = 0x33 + CALLVALUE = 0x34 + CALLDATALOAD = 0x35 + CALLDATASIZE = 0x36 + CALLDATACOPY = 0x37 + CODESIZE = 0x38 + CODECOPY = 0x39 + GASPRICE = 0x3A + EXTCODESIZE = 0x3B + EXTCODECOPY = 0x3C + RETURNDATASIZE = 0x3D + RETURNDATACOPY = 0x3E + EXTCODEHASH = 0x3F + + # Block Ops + BLOCKHASH = 0x40 + COINBASE = 0x41 + TIMESTAMP = 0x42 + NUMBER = 0x43 + PREVRANDAO = 0x44 + GASLIMIT = 0x45 + CHAINID = 0x46 + SELFBALANCE = 0x47 + BASEFEE = 0x48 + BLOBHASH = 0x49 + BLOBBASEFEE = 0x4A + SLOTNUM = 0x4B + + # Control Flow Ops + STOP = 0x00 + JUMP = 0x56 + JUMPI = 0x57 + PC = 0x58 + GAS = 0x5A + JUMPDEST = 0x5B + + # Storage Ops + SLOAD = 0x54 + SSTORE = 0x55 + TLOAD = 0x5C + TSTORE = 0x5D + + # Pop Operation + POP = 0x50 + + # Push Operations + PUSH0 = 0x5F + PUSH1 = 0x60 + PUSH2 = 0x61 + PUSH3 = 0x62 + PUSH4 = 0x63 + PUSH5 = 0x64 + PUSH6 = 0x65 + PUSH7 = 0x66 + PUSH8 = 0x67 + PUSH9 = 0x68 + PUSH10 = 0x69 + PUSH11 = 0x6A + PUSH12 = 0x6B + PUSH13 = 0x6C + PUSH14 = 0x6D + PUSH15 = 0x6E + PUSH16 = 0x6F + PUSH17 = 0x70 + PUSH18 = 0x71 + PUSH19 = 0x72 + PUSH20 = 0x73 + PUSH21 = 0x74 + PUSH22 = 0x75 + PUSH23 = 0x76 + PUSH24 = 0x77 + PUSH25 = 0x78 + PUSH26 = 0x79 + PUSH27 = 0x7A + PUSH28 = 0x7B + PUSH29 = 0x7C + PUSH30 = 0x7D + PUSH31 = 0x7E + PUSH32 = 0x7F + + # Dup operations + DUP1 = 0x80 + DUP2 = 0x81 + DUP3 = 0x82 + DUP4 = 0x83 + DUP5 = 0x84 + DUP6 = 0x85 + DUP7 = 0x86 + DUP8 = 0x87 + DUP9 = 0x88 + DUP10 = 0x89 + DUP11 = 0x8A + DUP12 = 0x8B + DUP13 = 0x8C + DUP14 = 0x8D + DUP15 = 0x8E + DUP16 = 0x8F + + # Swap operations + SWAP1 = 0x90 + SWAP2 = 0x91 + SWAP3 = 0x92 + SWAP4 = 0x93 + SWAP5 = 0x94 + SWAP6 = 0x95 + SWAP7 = 0x96 + SWAP8 = 0x97 + SWAP9 = 0x98 + SWAP10 = 0x99 + SWAP11 = 0x9A + SWAP12 = 0x9B + SWAP13 = 0x9C + SWAP14 = 0x9D + SWAP15 = 0x9E + SWAP16 = 0x9F + + # EIP-8024: Stack access instructions + DUPN = 0xE6 + SWAPN = 0xE7 + EXCHANGE = 0xE8 + + # Memory Operations + MLOAD = 0x51 + MSTORE = 0x52 + MSTORE8 = 0x53 + MSIZE = 0x59 + MCOPY = 0x5E + + # Log Operations + LOG0 = 0xA0 + LOG1 = 0xA1 + LOG2 = 0xA2 + LOG3 = 0xA3 + LOG4 = 0xA4 + + # System Operations + CREATE = 0xF0 + CALL = 0xF1 + CALLCODE = 0xF2 + RETURN = 0xF3 + DELEGATECALL = 0xF4 + CREATE2 = 0xF5 + STATICCALL = 0xFA + REVERT = 0xFD + SELFDESTRUCT = 0xFF + + +op_implementation: Dict[Ops, Callable] = { + Ops.STOP: control_flow_instructions.stop, + Ops.ADD: arithmetic_instructions.add, + Ops.MUL: arithmetic_instructions.mul, + Ops.SUB: arithmetic_instructions.sub, + Ops.DIV: arithmetic_instructions.div, + Ops.SDIV: arithmetic_instructions.sdiv, + Ops.MOD: arithmetic_instructions.mod, + Ops.SMOD: arithmetic_instructions.smod, + Ops.ADDMOD: arithmetic_instructions.addmod, + Ops.MULMOD: arithmetic_instructions.mulmod, + Ops.EXP: arithmetic_instructions.exp, + Ops.SIGNEXTEND: arithmetic_instructions.signextend, + Ops.LT: comparison_instructions.less_than, + Ops.GT: comparison_instructions.greater_than, + Ops.SLT: comparison_instructions.signed_less_than, + Ops.SGT: comparison_instructions.signed_greater_than, + Ops.EQ: comparison_instructions.equal, + Ops.ISZERO: comparison_instructions.is_zero, + Ops.AND: bitwise_instructions.bitwise_and, + Ops.OR: bitwise_instructions.bitwise_or, + Ops.XOR: bitwise_instructions.bitwise_xor, + Ops.NOT: bitwise_instructions.bitwise_not, + Ops.BYTE: bitwise_instructions.get_byte, + Ops.SHL: bitwise_instructions.bitwise_shl, + Ops.SHR: bitwise_instructions.bitwise_shr, + Ops.SAR: bitwise_instructions.bitwise_sar, + Ops.CLZ: bitwise_instructions.count_leading_zeros, + Ops.KECCAK: keccak_instructions.keccak, + Ops.SLOAD: storage_instructions.sload, + Ops.BLOCKHASH: block_instructions.block_hash, + Ops.COINBASE: block_instructions.coinbase, + Ops.TIMESTAMP: block_instructions.timestamp, + Ops.NUMBER: block_instructions.number, + Ops.PREVRANDAO: block_instructions.prev_randao, + Ops.GASLIMIT: block_instructions.gas_limit, + Ops.CHAINID: block_instructions.chain_id, + Ops.SLOTNUM: block_instructions.slot_number, + Ops.MLOAD: memory_instructions.mload, + Ops.MSTORE: memory_instructions.mstore, + Ops.MSTORE8: memory_instructions.mstore8, + Ops.MSIZE: memory_instructions.msize, + Ops.MCOPY: memory_instructions.mcopy, + Ops.ADDRESS: environment_instructions.address, + Ops.BALANCE: environment_instructions.balance, + Ops.ORIGIN: environment_instructions.origin, + Ops.CALLER: environment_instructions.caller, + Ops.CALLVALUE: environment_instructions.callvalue, + Ops.CALLDATALOAD: environment_instructions.calldataload, + Ops.CALLDATASIZE: environment_instructions.calldatasize, + Ops.CALLDATACOPY: environment_instructions.calldatacopy, + Ops.CODESIZE: environment_instructions.codesize, + Ops.CODECOPY: environment_instructions.codecopy, + Ops.GASPRICE: environment_instructions.gasprice, + Ops.EXTCODESIZE: environment_instructions.extcodesize, + Ops.EXTCODECOPY: environment_instructions.extcodecopy, + Ops.RETURNDATASIZE: environment_instructions.returndatasize, + Ops.RETURNDATACOPY: environment_instructions.returndatacopy, + Ops.EXTCODEHASH: environment_instructions.extcodehash, + Ops.SELFBALANCE: environment_instructions.self_balance, + Ops.BASEFEE: environment_instructions.base_fee, + Ops.BLOBHASH: environment_instructions.blob_hash, + Ops.BLOBBASEFEE: environment_instructions.blob_base_fee, + Ops.SSTORE: storage_instructions.sstore, + Ops.TLOAD: storage_instructions.tload, + Ops.TSTORE: storage_instructions.tstore, + Ops.JUMP: control_flow_instructions.jump, + Ops.JUMPI: control_flow_instructions.jumpi, + Ops.PC: control_flow_instructions.pc, + Ops.GAS: control_flow_instructions.gas_left, + Ops.JUMPDEST: control_flow_instructions.jumpdest, + Ops.POP: stack_instructions.pop, + Ops.PUSH0: stack_instructions.push0, + Ops.PUSH1: stack_instructions.push1, + Ops.PUSH2: stack_instructions.push2, + Ops.PUSH3: stack_instructions.push3, + Ops.PUSH4: stack_instructions.push4, + Ops.PUSH5: stack_instructions.push5, + Ops.PUSH6: stack_instructions.push6, + Ops.PUSH7: stack_instructions.push7, + Ops.PUSH8: stack_instructions.push8, + Ops.PUSH9: stack_instructions.push9, + Ops.PUSH10: stack_instructions.push10, + Ops.PUSH11: stack_instructions.push11, + Ops.PUSH12: stack_instructions.push12, + Ops.PUSH13: stack_instructions.push13, + Ops.PUSH14: stack_instructions.push14, + Ops.PUSH15: stack_instructions.push15, + Ops.PUSH16: stack_instructions.push16, + Ops.PUSH17: stack_instructions.push17, + Ops.PUSH18: stack_instructions.push18, + Ops.PUSH19: stack_instructions.push19, + Ops.PUSH20: stack_instructions.push20, + Ops.PUSH21: stack_instructions.push21, + Ops.PUSH22: stack_instructions.push22, + Ops.PUSH23: stack_instructions.push23, + Ops.PUSH24: stack_instructions.push24, + Ops.PUSH25: stack_instructions.push25, + Ops.PUSH26: stack_instructions.push26, + Ops.PUSH27: stack_instructions.push27, + Ops.PUSH28: stack_instructions.push28, + Ops.PUSH29: stack_instructions.push29, + Ops.PUSH30: stack_instructions.push30, + Ops.PUSH31: stack_instructions.push31, + Ops.PUSH32: stack_instructions.push32, + Ops.DUP1: stack_instructions.dup1, + Ops.DUP2: stack_instructions.dup2, + Ops.DUP3: stack_instructions.dup3, + Ops.DUP4: stack_instructions.dup4, + Ops.DUP5: stack_instructions.dup5, + Ops.DUP6: stack_instructions.dup6, + Ops.DUP7: stack_instructions.dup7, + Ops.DUP8: stack_instructions.dup8, + Ops.DUP9: stack_instructions.dup9, + Ops.DUP10: stack_instructions.dup10, + Ops.DUP11: stack_instructions.dup11, + Ops.DUP12: stack_instructions.dup12, + Ops.DUP13: stack_instructions.dup13, + Ops.DUP14: stack_instructions.dup14, + Ops.DUP15: stack_instructions.dup15, + Ops.DUP16: stack_instructions.dup16, + Ops.SWAP1: stack_instructions.swap1, + Ops.SWAP2: stack_instructions.swap2, + Ops.SWAP3: stack_instructions.swap3, + Ops.SWAP4: stack_instructions.swap4, + Ops.SWAP5: stack_instructions.swap5, + Ops.SWAP6: stack_instructions.swap6, + Ops.SWAP7: stack_instructions.swap7, + Ops.SWAP8: stack_instructions.swap8, + Ops.SWAP9: stack_instructions.swap9, + Ops.SWAP10: stack_instructions.swap10, + Ops.SWAP11: stack_instructions.swap11, + Ops.SWAP12: stack_instructions.swap12, + Ops.SWAP13: stack_instructions.swap13, + Ops.SWAP14: stack_instructions.swap14, + Ops.SWAP15: stack_instructions.swap15, + Ops.SWAP16: stack_instructions.swap16, + Ops.DUPN: stack_instructions.dupn, + Ops.SWAPN: stack_instructions.swapn, + Ops.EXCHANGE: stack_instructions.exchange, + Ops.LOG0: log_instructions.log0, + Ops.LOG1: log_instructions.log1, + Ops.LOG2: log_instructions.log2, + Ops.LOG3: log_instructions.log3, + Ops.LOG4: log_instructions.log4, + Ops.CREATE: system_instructions.create, + Ops.RETURN: system_instructions.return_, + Ops.CALL: system_instructions.call, + Ops.CALLCODE: system_instructions.callcode, + Ops.DELEGATECALL: system_instructions.delegatecall, + Ops.SELFDESTRUCT: system_instructions.selfdestruct, + Ops.STATICCALL: system_instructions.staticcall, + Ops.REVERT: system_instructions.revert, + Ops.CREATE2: system_instructions.create2, +} diff --git a/src/ethereum/forks/bogota/vm/instructions/arithmetic.py b/src/ethereum/forks/bogota/vm/instructions/arithmetic.py new file mode 100644 index 00000000000..4c7423cba8e --- /dev/null +++ b/src/ethereum/forks/bogota/vm/instructions/arithmetic.py @@ -0,0 +1,371 @@ +""" +Ethereum Virtual Machine (EVM) Arithmetic Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Arithmetic instructions. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.numeric import get_sign + +from .. import Evm +from ..gas import ( + GasCosts, + charge_gas, +) +from ..stack import pop, push + + +def add(evm: Evm) -> None: + """ + Adds the top two elements of the stack together, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_ADD) + + # OPERATION + result = x.wrapping_add(y) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def sub(evm: Evm) -> None: + """ + Subtracts the top two elements of the stack, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_SUB) + + # OPERATION + result = x.wrapping_sub(y) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mul(evm: Evm) -> None: + """ + Multiplies the top two elements of the stack, and pushes the result back + on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_MUL) + + # OPERATION + result = x.wrapping_mul(y) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def div(evm: Evm) -> None: + """ + Integer division of the top two elements of the stack. Pushes the result + back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + dividend = pop(evm.stack) + divisor = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_DIV) + + # OPERATION + if divisor == 0: + quotient = U256(0) + else: + quotient = dividend // divisor + + push(evm.stack, quotient) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +U255_CEIL_VALUE = 2**255 + + +def sdiv(evm: Evm) -> None: + """ + Signed integer division of the top two elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + dividend = pop(evm.stack).to_signed() + divisor = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GasCosts.OPCODE_SDIV) + + # OPERATION + if divisor == 0: + quotient = 0 + elif dividend == -U255_CEIL_VALUE and divisor == -1: + quotient = -U255_CEIL_VALUE + else: + sign = get_sign(dividend * divisor) + quotient = sign * (abs(dividend) // abs(divisor)) + + push(evm.stack, U256.from_signed(quotient)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mod(evm: Evm) -> None: + """ + Modulo remainder of the top two elements of the stack. Pushes the result + back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_MOD) + + # OPERATION + if y == 0: + remainder = U256(0) + else: + remainder = x % y + + push(evm.stack, remainder) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def smod(evm: Evm) -> None: + """ + Signed modulo remainder of the top two elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack).to_signed() + y = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GasCosts.OPCODE_SMOD) + + # OPERATION + if y == 0: + remainder = 0 + else: + remainder = get_sign(x) * (abs(x) % abs(y)) + + push(evm.stack, U256.from_signed(remainder)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def addmod(evm: Evm) -> None: + """ + Modulo addition of the top 2 elements with the 3rd element. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = Uint(pop(evm.stack)) + y = Uint(pop(evm.stack)) + z = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GasCosts.OPCODE_ADDMOD) + + # OPERATION + if z == 0: + result = U256(0) + else: + result = U256((x + y) % z) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mulmod(evm: Evm) -> None: + """ + Modulo multiplication of the top 2 elements with the 3rd element. Pushes + the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = Uint(pop(evm.stack)) + y = Uint(pop(evm.stack)) + z = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GasCosts.OPCODE_MULMOD) + + # OPERATION + if z == 0: + result = U256(0) + else: + result = U256((x * y) % z) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def exp(evm: Evm) -> None: + """ + Exponential operation of the top 2 elements. Pushes the result back on + the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + base = Uint(pop(evm.stack)) + exponent = Uint(pop(evm.stack)) + + # GAS + # This is equivalent to 1 + floor(log(y, 256)). But in python the log + # function is inaccurate leading to wrong results. + exponent_bits = exponent.bit_length() + exponent_bytes = (exponent_bits + Uint(7)) // Uint(8) + charge_gas( + evm, + GasCosts.OPCODE_EXP_BASE + + GasCosts.OPCODE_EXP_PER_BYTE * exponent_bytes, + ) + + # OPERATION + result = U256(pow(base, exponent, Uint(U256.MAX_VALUE) + Uint(1))) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def signextend(evm: Evm) -> None: + """ + Sign extend operation. In other words, extend a signed number which + fits in N bytes to 32 bytes. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + byte_num = pop(evm.stack) + value = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_SIGNEXTEND) + + # OPERATION + if byte_num > U256(31): + # Can't extend any further + result = value + else: + # U256(0).to_be_bytes() gives b'' instead of b'\x00'. + value_bytes = Bytes(value.to_be_bytes32()) + # Now among the obtained value bytes, consider only + # N `least significant bytes`, where N is `byte_num + 1`. + value_bytes = value_bytes[31 - int(byte_num) :] + sign_bit = value_bytes[0] >> 7 + if sign_bit == 0: + result = U256.from_be_bytes(value_bytes) + else: + num_bytes_prepend = U256(32) - (byte_num + U256(1)) + result = U256.from_be_bytes( + bytearray([0xFF] * num_bytes_prepend) + value_bytes + ) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/bogota/vm/instructions/bitwise.py b/src/ethereum/forks/bogota/vm/instructions/bitwise.py new file mode 100644 index 00000000000..7674d3c720f --- /dev/null +++ b/src/ethereum/forks/bogota/vm/instructions/bitwise.py @@ -0,0 +1,277 @@ +""" +Ethereum Virtual Machine (EVM) Bitwise Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM bitwise instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from .. import Evm +from ..gas import ( + GasCosts, + charge_gas, +) +from ..stack import pop, push + + +def bitwise_and(evm: Evm) -> None: + """ + Bitwise AND operation of the top 2 elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_AND) + + # OPERATION + push(evm.stack, x & y) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_or(evm: Evm) -> None: + """ + Bitwise OR operation of the top 2 elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_OR) + + # OPERATION + push(evm.stack, x | y) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_xor(evm: Evm) -> None: + """ + Bitwise XOR operation of the top 2 elements of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + y = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_XOR) + + # OPERATION + push(evm.stack, x ^ y) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_not(evm: Evm) -> None: + """ + Bitwise NOT operation of the top element of the stack. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_NOT) + + # OPERATION + push(evm.stack, ~x) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def get_byte(evm: Evm) -> None: + """ + For a word (defined by next top element of the stack), retrieve the + Nth byte (0-indexed and defined by top element of stack) from the + left (most significant) to right (least significant). + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + byte_index = pop(evm.stack) + word = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_BYTE) + + # OPERATION + if byte_index >= U256(32): + result = U256(0) + else: + extra_bytes_to_right = U256(31) - byte_index + # Remove the extra bytes in the right + word = word >> (extra_bytes_to_right * U256(8)) + # Remove the extra bytes in the left + word = word & U256(0xFF) + result = word + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_shl(evm: Evm) -> None: + """ + Logical shift left (SHL) operation of the top 2 elements of the stack. + Pushes the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + shift = Uint(pop(evm.stack)) + value = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GasCosts.OPCODE_SHL) + + # OPERATION + if shift < Uint(256): + result = U256((value << shift) & Uint(U256.MAX_VALUE)) + else: + result = U256(0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_shr(evm: Evm) -> None: + """ + Logical shift right (SHR) operation of the top 2 elements of the stack. + Pushes the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + shift = pop(evm.stack) + value = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_SHR) + + # OPERATION + if shift < U256(256): + result = value >> shift + else: + result = U256(0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def bitwise_sar(evm: Evm) -> None: + """ + Arithmetic shift right (SAR) operation of the top 2 elements of the stack. + Pushes the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + shift = int(pop(evm.stack)) + signed_value = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GasCosts.OPCODE_SAR) + + # OPERATION + if shift < 256: + result = U256.from_signed(signed_value >> shift) + elif signed_value >= 0: + result = U256(0) + else: + result = U256.MAX_VALUE + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def count_leading_zeros(evm: Evm) -> None: + """ + Count the number of leading zero bits in a 256-bit word. + + Pops one value from the stack and pushes the number of leading zero bits. + If the input is zero, pushes 256. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_CLZ) + + # OPERATION + bit_length = U256(x.bit_length()) + result = U256(256) - bit_length + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/bogota/vm/instructions/block.py b/src/ethereum/forks/bogota/vm/instructions/block.py new file mode 100644 index 00000000000..eeb3f7756b0 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/instructions/block.py @@ -0,0 +1,294 @@ +""" +Ethereum Virtual Machine (EVM) Block Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM block instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from .. import Evm +from ..gas import GasCosts, charge_gas +from ..stack import pop, push + + +def block_hash(evm: Evm) -> None: + """ + Push the hash of one of the 256 most recent complete blocks onto the + stack. The block number to hash is present at the top of the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.bogota.vm.exceptions.StackUnderflowError` + If `len(stack)` is less than `1`. + :py:class:`~ethereum.forks.bogota.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `20`. + + """ + # STACK + block_number = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GasCosts.OPCODE_BLOCKHASH) + + # OPERATION + max_block_number = block_number + Uint(256) + current_block_number = evm.message.block_env.number + if ( + current_block_number <= block_number + or current_block_number > max_block_number + ): + # Default hash to 0, if the block of interest is not yet on the chain + # (including the block which has the current executing transaction), + # or if the block's age is more than 256. + current_block_hash = b"\x00" + else: + current_block_hash = evm.message.block_env.block_hashes[ + -(current_block_number - block_number) + ] + + push(evm.stack, U256.from_be_bytes(current_block_hash)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def coinbase(evm: Evm) -> None: + """ + Push the current block's beneficiary address (address of the block miner) + onto the stack. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.bogota.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.bogota.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_COINBASE) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.block_env.coinbase)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def timestamp(evm: Evm) -> None: + """ + Push the current block's timestamp onto the stack. Here the timestamp + being referred to is actually the unix timestamp in seconds. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.bogota.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.bogota.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_TIMESTAMP) + + # OPERATION + push(evm.stack, evm.message.block_env.time) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def number(evm: Evm) -> None: + """ + Push the current block's number onto the stack. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.bogota.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.bogota.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_NUMBER) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.number)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def prev_randao(evm: Evm) -> None: + """ + Push the `prev_randao` value onto the stack. + + The `prev_randao` value is the random output of the beacon chain's + randomness oracle for the previous block. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.bogota.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.bogota.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_PREVRANDAO) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.block_env.prev_randao)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def gas_limit(evm: Evm) -> None: + """ + Push the current block's gas limit onto the stack. + + Here the current block refers to the block in which the currently + executing transaction/call resides. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.bogota.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.bogota.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_GASLIMIT) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.block_gas_limit)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def chain_id(evm: Evm) -> None: + """ + Push the chain id onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.bogota.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.bogota.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_CHAINID) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.chain_id)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def slot_number(evm: Evm) -> None: + """ + Push the current slot number onto the stack. + + The slot number is provided by the consensus layer and passed to the + execution layer through the engine API. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.bogota.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.bogota.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_SLOTNUM) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.slot_number)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/bogota/vm/instructions/comparison.py b/src/ethereum/forks/bogota/vm/instructions/comparison.py new file mode 100644 index 00000000000..22d3d8916b1 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/instructions/comparison.py @@ -0,0 +1,180 @@ +""" +Ethereum Virtual Machine (EVM) Comparison Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Comparison instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from .. import Evm +from ..gas import ( + GasCosts, + charge_gas, +) +from ..stack import pop, push + + +def less_than(evm: Evm) -> None: + """ + Checks if the top element is less than the next top element. Pushes the + result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack) + right = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_LT) + + # OPERATION + result = U256(left < right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def signed_less_than(evm: Evm) -> None: + """ + Signed less-than comparison. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack).to_signed() + right = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GasCosts.OPCODE_SLT) + + # OPERATION + result = U256(left < right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def greater_than(evm: Evm) -> None: + """ + Checks if the top element is greater than the next top element. Pushes + the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack) + right = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_GT) + + # OPERATION + result = U256(left > right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def signed_greater_than(evm: Evm) -> None: + """ + Signed greater-than comparison. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack).to_signed() + right = pop(evm.stack).to_signed() + + # GAS + charge_gas(evm, GasCosts.OPCODE_SGT) + + # OPERATION + result = U256(left > right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def equal(evm: Evm) -> None: + """ + Checks if the top element is equal to the next top element. Pushes + the result back on the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + left = pop(evm.stack) + right = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_EQ) + + # OPERATION + result = U256(left == right) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def is_zero(evm: Evm) -> None: + """ + Checks if the top element is equal to 0. Pushes the result back on the + stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + x = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_ISZERO) + + # OPERATION + result = U256(x == 0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/bogota/vm/instructions/control_flow.py b/src/ethereum/forks/bogota/vm/instructions/control_flow.py new file mode 100644 index 00000000000..548a05d3163 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/instructions/control_flow.py @@ -0,0 +1,174 @@ +""" +Ethereum Virtual Machine (EVM) Control Flow Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM control flow instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from ...vm.gas import ( + GasCosts, + charge_gas, +) +from .. import Evm +from ..exceptions import InvalidJumpDestError +from ..stack import pop, push + + +def stop(evm: Evm) -> None: + """ + Stop further execution of EVM code. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + pass + + # OPERATION + evm.running = False + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def jump(evm: Evm) -> None: + """ + Alter the program counter to the location specified by the top of the + stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + jump_dest = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GasCosts.OPCODE_JUMP) + + # OPERATION + if jump_dest not in evm.valid_jump_destinations: + raise InvalidJumpDestError + + # PROGRAM COUNTER + evm.pc = Uint(jump_dest) + + +def jumpi(evm: Evm) -> None: + """ + Alter the program counter to the specified location if and only if a + condition is true. If the condition is not true, then the program counter + would increase only by 1. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + jump_dest = Uint(pop(evm.stack)) + conditional_value = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_JUMPI) + + # OPERATION + if conditional_value == 0: + destination = evm.pc + Uint(1) + elif jump_dest not in evm.valid_jump_destinations: + raise InvalidJumpDestError + else: + destination = jump_dest + + # PROGRAM COUNTER + evm.pc = destination + + +def pc(evm: Evm) -> None: + """ + Push onto the stack the value of the program counter after reaching the + current instruction and without increasing it for the next instruction. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_PC) + + # OPERATION + push(evm.stack, U256(evm.pc)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def gas_left(evm: Evm) -> None: + """ + Push the amount of available gas (including the corresponding reduction + for the cost of this instruction) onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_GAS) + + # OPERATION + push(evm.stack, U256(evm.gas_left)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def jumpdest(evm: Evm) -> None: + """ + Mark a valid destination for jumps. This is a noop, present only + to be used by `JUMP` and `JUMPI` opcodes to verify that their jump is + valid. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_JUMPDEST) + + # OPERATION + pass + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/bogota/vm/instructions/environment.py b/src/ethereum/forks/bogota/vm/instructions/environment.py new file mode 100644 index 00000000000..8a7e9ec1486 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/instructions/environment.py @@ -0,0 +1,611 @@ +""" +Ethereum Virtual Machine (EVM) Environmental Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM environment related instructions. +""" + +from ethereum_types.bytes import Bytes32 +from ethereum_types.numeric import U256, Uint, ulen + +from ethereum.state import EMPTY_ACCOUNT +from ethereum.utils.numeric import ceil32 + +from ...state_tracker import get_account, get_code +from ...utils.address import to_address_masked +from ...vm.memory import buffer_read, memory_write +from .. import Evm +from ..exceptions import OutOfBoundsRead +from ..gas import ( + GasCosts, + calculate_blob_gas_price, + calculate_gas_extend_memory, + charge_gas, +) +from ..stack import pop, push + + +def address(evm: Evm) -> None: + """ + Pushes the address of the current executing account to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_ADDRESS) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.current_target)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def balance(evm: Evm) -> None: + """ + Pushes the balance of the given account onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address_masked(pop(evm.stack)) + + # GAS + if address in evm.accessed_addresses: + charge_gas(evm, GasCosts.WARM_ACCESS) + else: + evm.accessed_addresses.add(address) + charge_gas(evm, GasCosts.COLD_ACCOUNT_ACCESS) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. + tx_state = evm.message.tx_env.state + balance = get_account(tx_state, address).balance + + push(evm.stack, balance) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def origin(evm: Evm) -> None: + """ + Pushes the address of the original transaction sender to the stack. + The origin address can only be an EOA. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_ORIGIN) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.tx_env.origin)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def caller(evm: Evm) -> None: + """ + Pushes the address of the caller onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_CALLER) + + # OPERATION + push(evm.stack, U256.from_be_bytes(evm.message.caller)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def callvalue(evm: Evm) -> None: + """ + Push the value (in wei) sent with the call onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_CALLVALUE) + + # OPERATION + push(evm.stack, evm.message.value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def calldataload(evm: Evm) -> None: + """ + Push a word (32 bytes) of the input data belonging to the current + environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_index = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_CALLDATALOAD) + + # OPERATION + value = buffer_read(evm.message.data, start_index, U256(32)) + + push(evm.stack, U256.from_be_bytes(value)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def calldatasize(evm: Evm) -> None: + """ + Push the size of input data in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_CALLDATASIZE) + + # OPERATION + push(evm.stack, U256(len(evm.message.data))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def calldatacopy(evm: Evm) -> None: + """ + Copy a portion of the input data in current environment to memory. + + This will also expand the memory, in case that the memory is insufficient + to store the data. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + data_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GasCosts.OPCODE_COPY_PER_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GasCosts.OPCODE_CALLDATACOPY_BASE + copy_gas_cost + extend_memory.cost, + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + value = buffer_read(evm.message.data, data_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def codesize(evm: Evm) -> None: + """ + Push the size of code running in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_CODESIZE) + + # OPERATION + push(evm.stack, U256(len(evm.code))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def codecopy(evm: Evm) -> None: + """ + Copy a portion of the code in current environment to memory. + + This will also expand the memory, in case that the memory is insufficient + to store the data. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + code_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GasCosts.OPCODE_COPY_PER_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GasCosts.OPCODE_CODECOPY_BASE + copy_gas_cost + extend_memory.cost, + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + value = buffer_read(evm.code, code_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def gasprice(evm: Evm) -> None: + """ + Push the gas price used in current environment onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_GASPRICE) + + # OPERATION + push(evm.stack, U256(evm.message.tx_env.gas_price)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def extcodesize(evm: Evm) -> None: + """ + Push the code size of a given account onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address_masked(pop(evm.stack)) + + # GAS + if address in evm.accessed_addresses: + access_gas_cost = GasCosts.WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + access_gas_cost += GasCosts.WARM_ACCESS # Code reading cost (EIP-8038) + charge_gas(evm, access_gas_cost) + + # OPERATION + tx_state = evm.message.tx_env.state + code_hash = get_account(tx_state, address).code_hash + code = get_code(tx_state, code_hash) + + codesize = U256(len(code)) + push(evm.stack, codesize) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def extcodecopy(evm: Evm) -> None: + """ + Copy a portion of an account's code to memory. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address_masked(pop(evm.stack)) + memory_start_index = pop(evm.stack) + code_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GasCosts.OPCODE_COPY_PER_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + + if address in evm.accessed_addresses: + access_gas_cost = GasCosts.WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + access_gas_cost += GasCosts.WARM_ACCESS # Code reading cost (EIP-8038) + + total_gas_cost = access_gas_cost + copy_gas_cost + extend_memory.cost + + charge_gas(evm, total_gas_cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + tx_state = evm.message.tx_env.state + code_hash = get_account(tx_state, address).code_hash + code = get_code(tx_state, code_hash) + + value = buffer_read(code, code_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def returndatasize(evm: Evm) -> None: + """ + Pushes the size of the return data buffer onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_RETURNDATASIZE) + + # OPERATION + push(evm.stack, U256(len(evm.return_data))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def returndatacopy(evm: Evm) -> None: + """ + Copies data from the return data buffer to memory. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + return_data_start_position = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GasCosts.OPCODE_RETURNDATACOPY_PER_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GasCosts.OPCODE_RETURNDATACOPY_BASE + + copy_gas_cost + + extend_memory.cost, + ) + if Uint(return_data_start_position) + Uint(size) > ulen(evm.return_data): + raise OutOfBoundsRead + + evm.memory += b"\x00" * extend_memory.expand_by + value = evm.return_data[ + return_data_start_position : return_data_start_position + size + ] + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def extcodehash(evm: Evm) -> None: + """ + Returns the keccak256 hash of a contract’s bytecode. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + address = to_address_masked(pop(evm.stack)) + + # GAS + if address in evm.accessed_addresses: + access_gas_cost = GasCosts.WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) + + # OPERATION + tx_state = evm.message.tx_env.state + account = get_account(tx_state, address) + + if account == EMPTY_ACCOUNT: + codehash = U256(0) + else: + codehash = U256.from_be_bytes(account.code_hash) + + push(evm.stack, codehash) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def self_balance(evm: Evm) -> None: + """ + Pushes the balance of the current address to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.FAST_STEP) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. + balance = get_account( + evm.message.tx_env.state, evm.message.current_target + ).balance + + push(evm.stack, balance) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def base_fee(evm: Evm) -> None: + """ + Pushes the base fee of the current block on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_BASEFEE) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.base_fee_per_gas)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def blob_hash(evm: Evm) -> None: + """ + Pushes the versioned hash at a particular index on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + index = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_BLOBHASH) + + # OPERATION + if int(index) < len(evm.message.tx_env.blob_versioned_hashes): + blob_hash = evm.message.tx_env.blob_versioned_hashes[index] + else: + blob_hash = Bytes32(b"\x00" * 32) + push(evm.stack, U256.from_be_bytes(blob_hash)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def blob_base_fee(evm: Evm) -> None: + """ + Pushes the blob base fee on to the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_BLOBBASEFEE) + + # OPERATION + blob_base_fee = calculate_blob_gas_price( + evm.message.block_env.excess_blob_gas + ) + push(evm.stack, U256(blob_base_fee)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/bogota/vm/instructions/keccak.py b/src/ethereum/forks/bogota/vm/instructions/keccak.py new file mode 100644 index 00000000000..0d3e17cf08e --- /dev/null +++ b/src/ethereum/forks/bogota/vm/instructions/keccak.py @@ -0,0 +1,65 @@ +""" +Ethereum Virtual Machine (EVM) Keccak Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM keccak instructions. +""" + +from ethereum_types.numeric import U256, Uint + +from ethereum.crypto.hash import keccak256 +from ethereum.utils.numeric import ceil32 + +from .. import Evm +from ..gas import ( + GasCosts, + calculate_gas_extend_memory, + charge_gas, +) +from ..memory import memory_read_bytes +from ..stack import pop, push + + +def keccak(evm: Evm) -> None: + """ + Pushes to the stack the Keccak-256 hash of a region of memory. + + This also expands the memory, in case the memory is insufficient to + access the data's memory location. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + word_gas_cost = GasCosts.OPCODE_KECCAK256_PER_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GasCosts.OPCODE_KECCAK256_BASE + word_gas_cost + extend_memory.cost, + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + data = memory_read_bytes(evm.memory, memory_start_index, size) + hashed = keccak256(data) + + push(evm.stack, U256.from_be_bytes(hashed)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/bogota/vm/instructions/log.py b/src/ethereum/forks/bogota/vm/instructions/log.py new file mode 100644 index 00000000000..695f4de735c --- /dev/null +++ b/src/ethereum/forks/bogota/vm/instructions/log.py @@ -0,0 +1,87 @@ +""" +Ethereum Virtual Machine (EVM) Logging Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM logging instructions. +""" + +from functools import partial +from typing import Callable + +from ethereum_types.numeric import Uint + +from ...blocks import Log +from .. import Evm +from ..exceptions import WriteInStaticContext +from ..gas import ( + GasCosts, + calculate_gas_extend_memory, + charge_gas, +) +from ..memory import memory_read_bytes +from ..stack import pop + + +def log_n(evm: Evm, num_topics: int) -> None: + """ + Appends a log entry, having `num_topics` topics, to the evm logs. + + This will also expand the memory if the data (required by the log entry) + corresponding to the memory is not accessible. + + Parameters + ---------- + evm : + The current EVM frame. + num_topics : + The number of topics to be included in the log entry. + + """ + # STACK + memory_start_index = pop(evm.stack) + size = pop(evm.stack) + + topics = [] + for _ in range(num_topics): + topic = pop(evm.stack).to_be_bytes32() + topics.append(topic) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GasCosts.OPCODE_LOG_BASE + + GasCosts.OPCODE_LOG_DATA_PER_BYTE * Uint(size) + + GasCosts.OPCODE_LOG_TOPIC * Uint(num_topics) + + extend_memory.cost, + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + if evm.message.is_static: + raise WriteInStaticContext + log_entry = Log( + address=evm.message.current_target, + topics=tuple(topics), + data=memory_read_bytes(evm.memory, memory_start_index, size), + ) + + evm.logs = evm.logs + (log_entry,) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +log0: Callable[[Evm], None] = partial(log_n, num_topics=0) +log1: Callable[[Evm], None] = partial(log_n, num_topics=1) +log2: Callable[[Evm], None] = partial(log_n, num_topics=2) +log3: Callable[[Evm], None] = partial(log_n, num_topics=3) +log4: Callable[[Evm], None] = partial(log_n, num_topics=4) diff --git a/src/ethereum/forks/bogota/vm/instructions/memory.py b/src/ethereum/forks/bogota/vm/instructions/memory.py new file mode 100644 index 00000000000..bba3ddf19d5 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/instructions/memory.py @@ -0,0 +1,178 @@ +""" +Ethereum Virtual Machine (EVM) Memory Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM Memory instructions. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.numeric import ceil32 + +from .. import Evm +from ..gas import ( + GasCosts, + calculate_gas_extend_memory, + charge_gas, +) +from ..memory import memory_read_bytes, memory_write +from ..stack import pop, push + + +def mstore(evm: Evm) -> None: + """ + Stores a word to memory. + This also expands the memory, if the memory is + insufficient to store the word. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_position = pop(evm.stack) + value = pop(evm.stack).to_be_bytes32() + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(start_position, U256(len(value)))] + ) + + charge_gas(evm, GasCosts.OPCODE_MSTORE_BASE + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + memory_write(evm.memory, start_position, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mstore8(evm: Evm) -> None: + """ + Stores a byte to memory. + This also expands the memory, if the memory is + insufficient to store the word. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_position = pop(evm.stack) + value = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(start_position, U256(1))] + ) + + charge_gas(evm, GasCosts.OPCODE_MSTORE8_BASE + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + normalized_bytes_value = Bytes([value & U256(0xFF)]) + memory_write(evm.memory, start_position, normalized_bytes_value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mload(evm: Evm) -> None: + """ + Loads a word from memory. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_position = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(start_position, U256(32))] + ) + charge_gas(evm, GasCosts.OPCODE_MLOAD_BASE + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + value = U256.from_be_bytes( + memory_read_bytes(evm.memory, start_position, U256(32)) + ) + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def msize(evm: Evm) -> None: + """ + Pushes the size of active memory in bytes onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_MSIZE) + + # OPERATION + push(evm.stack, U256(len(evm.memory))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mcopy(evm: Evm) -> None: + """ + Copies the bytes in memory from one location to another. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + destination = pop(evm.stack) + source = pop(evm.stack) + length = pop(evm.stack) + + # GAS + words = ceil32(Uint(length)) // Uint(32) + copy_gas_cost = GasCosts.OPCODE_COPY_PER_WORD * words + + extend_memory = calculate_gas_extend_memory( + evm.memory, [(source, length), (destination, length)] + ) + charge_gas( + evm, + GasCosts.OPCODE_MCOPY_BASE + copy_gas_cost + extend_memory.cost, + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + value = memory_read_bytes(evm.memory, source, length) + memory_write(evm.memory, destination, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/bogota/vm/instructions/stack.py b/src/ethereum/forks/bogota/vm/instructions/stack.py new file mode 100644 index 00000000000..0e72bd01f31 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/instructions/stack.py @@ -0,0 +1,317 @@ +""" +Ethereum Virtual Machine (EVM) Stack Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM stack related instructions. +""" + +from functools import partial +from typing import Callable + +from ethereum_types.numeric import U8, U256, Uint + +from .. import Evm, stack +from ..exceptions import StackUnderflowError +from ..gas import ( + GasCosts, + charge_gas, +) +from ..memory import buffer_read +from ..stack import decode_pair, decode_single + + +def pop(evm: Evm) -> None: + """ + Removes an item from the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + stack.pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_POP) + + # OPERATION + pass + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def push_n(evm: Evm, num_bytes: int) -> None: + """ + Pushes an N-byte immediate onto the stack. Push zero if num_bytes is zero. + + Parameters + ---------- + evm : + The current EVM frame. + + num_bytes : + The number of immediate bytes to be read from the code and pushed to + the stack. Push zero if num_bytes is zero. + + """ + # STACK + pass + + # GAS + if num_bytes == 0: + charge_gas(evm, GasCosts.OPCODE_PUSH0) + else: + charge_gas(evm, GasCosts.OPCODE_PUSH) + + # OPERATION + data_to_push = U256.from_be_bytes( + buffer_read(evm.code, U256(evm.pc + Uint(1)), U256(num_bytes)) + ) + stack.push(evm.stack, data_to_push) + + # PROGRAM COUNTER + evm.pc += Uint(1) + Uint(num_bytes) + + +def dup_n(evm: Evm, item_number: int) -> None: + """ + Duplicates the Nth stack item (from top of the stack) to the top of stack. + + Parameters + ---------- + evm : + The current EVM frame. + + item_number : + The stack item number (0-indexed from top of stack) to be duplicated + to the top of stack. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_DUP) + if item_number >= len(evm.stack): + raise StackUnderflowError + data_to_duplicate = evm.stack[len(evm.stack) - 1 - item_number] + stack.push(evm.stack, data_to_duplicate) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def swap_n(evm: Evm, item_number: int) -> None: + """ + Swaps the top and the `item_number` element of the stack, where + the top of the stack is position zero. + + If `item_number` is zero, this function does nothing (which should not be + possible, since there is no `SWAP0` instruction). + + Parameters + ---------- + evm : + The current EVM frame. + + item_number : + The stack item number (0-indexed from top of stack) to be swapped + with the top of stack element. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_SWAP) + if item_number >= len(evm.stack): + raise StackUnderflowError + evm.stack[-1], evm.stack[-1 - item_number] = ( + evm.stack[-1 - item_number], + evm.stack[-1], + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +push0: Callable[[Evm], None] = partial(push_n, num_bytes=0) +push1: Callable[[Evm], None] = partial(push_n, num_bytes=1) +push2: Callable[[Evm], None] = partial(push_n, num_bytes=2) +push3: Callable[[Evm], None] = partial(push_n, num_bytes=3) +push4: Callable[[Evm], None] = partial(push_n, num_bytes=4) +push5: Callable[[Evm], None] = partial(push_n, num_bytes=5) +push6: Callable[[Evm], None] = partial(push_n, num_bytes=6) +push7: Callable[[Evm], None] = partial(push_n, num_bytes=7) +push8: Callable[[Evm], None] = partial(push_n, num_bytes=8) +push9: Callable[[Evm], None] = partial(push_n, num_bytes=9) +push10: Callable[[Evm], None] = partial(push_n, num_bytes=10) +push11: Callable[[Evm], None] = partial(push_n, num_bytes=11) +push12: Callable[[Evm], None] = partial(push_n, num_bytes=12) +push13: Callable[[Evm], None] = partial(push_n, num_bytes=13) +push14: Callable[[Evm], None] = partial(push_n, num_bytes=14) +push15: Callable[[Evm], None] = partial(push_n, num_bytes=15) +push16: Callable[[Evm], None] = partial(push_n, num_bytes=16) +push17: Callable[[Evm], None] = partial(push_n, num_bytes=17) +push18: Callable[[Evm], None] = partial(push_n, num_bytes=18) +push19: Callable[[Evm], None] = partial(push_n, num_bytes=19) +push20: Callable[[Evm], None] = partial(push_n, num_bytes=20) +push21: Callable[[Evm], None] = partial(push_n, num_bytes=21) +push22: Callable[[Evm], None] = partial(push_n, num_bytes=22) +push23: Callable[[Evm], None] = partial(push_n, num_bytes=23) +push24: Callable[[Evm], None] = partial(push_n, num_bytes=24) +push25: Callable[[Evm], None] = partial(push_n, num_bytes=25) +push26: Callable[[Evm], None] = partial(push_n, num_bytes=26) +push27: Callable[[Evm], None] = partial(push_n, num_bytes=27) +push28: Callable[[Evm], None] = partial(push_n, num_bytes=28) +push29: Callable[[Evm], None] = partial(push_n, num_bytes=29) +push30: Callable[[Evm], None] = partial(push_n, num_bytes=30) +push31: Callable[[Evm], None] = partial(push_n, num_bytes=31) +push32: Callable[[Evm], None] = partial(push_n, num_bytes=32) + +dup1: Callable[[Evm], None] = partial(dup_n, item_number=0) +dup2: Callable[[Evm], None] = partial(dup_n, item_number=1) +dup3: Callable[[Evm], None] = partial(dup_n, item_number=2) +dup4: Callable[[Evm], None] = partial(dup_n, item_number=3) +dup5: Callable[[Evm], None] = partial(dup_n, item_number=4) +dup6: Callable[[Evm], None] = partial(dup_n, item_number=5) +dup7: Callable[[Evm], None] = partial(dup_n, item_number=6) +dup8: Callable[[Evm], None] = partial(dup_n, item_number=7) +dup9: Callable[[Evm], None] = partial(dup_n, item_number=8) +dup10: Callable[[Evm], None] = partial(dup_n, item_number=9) +dup11: Callable[[Evm], None] = partial(dup_n, item_number=10) +dup12: Callable[[Evm], None] = partial(dup_n, item_number=11) +dup13: Callable[[Evm], None] = partial(dup_n, item_number=12) +dup14: Callable[[Evm], None] = partial(dup_n, item_number=13) +dup15: Callable[[Evm], None] = partial(dup_n, item_number=14) +dup16: Callable[[Evm], None] = partial(dup_n, item_number=15) + +swap1: Callable[[Evm], None] = partial(swap_n, item_number=1) +swap2: Callable[[Evm], None] = partial(swap_n, item_number=2) +swap3: Callable[[Evm], None] = partial(swap_n, item_number=3) +swap4: Callable[[Evm], None] = partial(swap_n, item_number=4) +swap5: Callable[[Evm], None] = partial(swap_n, item_number=5) +swap6: Callable[[Evm], None] = partial(swap_n, item_number=6) +swap7: Callable[[Evm], None] = partial(swap_n, item_number=7) +swap8: Callable[[Evm], None] = partial(swap_n, item_number=8) +swap9: Callable[[Evm], None] = partial(swap_n, item_number=9) +swap10: Callable[[Evm], None] = partial(swap_n, item_number=10) +swap11: Callable[[Evm], None] = partial(swap_n, item_number=11) +swap12: Callable[[Evm], None] = partial(swap_n, item_number=12) +swap13: Callable[[Evm], None] = partial(swap_n, item_number=13) +swap14: Callable[[Evm], None] = partial(swap_n, item_number=14) +swap15: Callable[[Evm], None] = partial(swap_n, item_number=15) +swap16: Callable[[Evm], None] = partial(swap_n, item_number=16) + + +def dupn(evm: Evm) -> None: + """ + Duplicate the Nth stack item (from top of the stack) to the top of stack. + The item number is read from the immediate byte following the opcode and + decoded using the EIP-8024 index shifting rules. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_DUPN) + + # OPERATION + immediate_data = U8( + buffer_read(evm.code, U256(evm.pc + Uint(1)), U256(1))[0] + ) + item_number = decode_single(immediate_data) + if int(item_number) > len(evm.stack): + raise StackUnderflowError + data_to_duplicate = evm.stack[-item_number] + stack.push(evm.stack, data_to_duplicate) + + # PROGRAM COUNTER + evm.pc += Uint(2) + + +def swapn(evm: Evm) -> None: + """ + Swap the top stack item with the Nth stack item. + The value N is read from the immediate byte following the opcode and + decoded using the EIP-8024 index shifting rules. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_SWAPN) + + # OPERATION + immediate_data = U8( + buffer_read(evm.code, U256(evm.pc + Uint(1)), U256(1))[0] + ) + item_number = decode_single(immediate_data) + # SWAPN with decoded value n swaps top (position 1) with position (n+1) + if int(item_number) + 1 > len(evm.stack): + raise StackUnderflowError + # stack[-1] is top (position 1), stack[-(item_number+1)] is position (n+1) + evm.stack[-1], evm.stack[-(item_number + U8(1))] = ( + evm.stack[-(item_number + U8(1))], + evm.stack[-1], + ) + + # PROGRAM COUNTER + evm.pc += Uint(2) + + +def exchange(evm: Evm) -> None: + """ + Exchange the Nth stack item with the Mth stack item. + The values N and M are decoded from the immediate byte using the + EIP-8024 index shifting rules. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_EXCHANGE) + + # OPERATION + immediate_data = U8( + buffer_read(evm.code, U256(evm.pc + Uint(1)), U256(1))[0] + ) + n, m = decode_pair(immediate_data) + # EXCHANGE swaps position (n+1) with position (m+1) + depth = max(n, m) + U8(1) + if int(depth) > len(evm.stack): + raise StackUnderflowError + evm.stack[-(n + U8(1))], evm.stack[-(m + U8(1))] = ( + evm.stack[-(m + U8(1))], + evm.stack[-(n + U8(1))], + ) + + # PROGRAM COUNTER + evm.pc += Uint(2) diff --git a/src/ethereum/forks/bogota/vm/instructions/storage.py b/src/ethereum/forks/bogota/vm/instructions/storage.py new file mode 100644 index 00000000000..91aec91163d --- /dev/null +++ b/src/ethereum/forks/bogota/vm/instructions/storage.py @@ -0,0 +1,196 @@ +""" +Ethereum Virtual Machine (EVM) Storage Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM storage related instructions. +""" + +from ethereum_types.numeric import Uint + +from ...fork_types import StateGas +from ...state_tracker import ( + get_storage, + get_storage_original, + get_transient_storage, + set_storage, + set_transient_storage, +) +from .. import Evm, credit_state_gas_refund +from ..exceptions import WriteInStaticContext +from ..gas import ( + GasCosts, + StateGasCosts, + charge_gas, + charge_state_gas, + check_gas, +) +from ..stack import pop, push + + +def sload(evm: Evm) -> None: + """ + Loads to the stack, the value corresponding to a certain key from the + storage of the current account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + + # GAS + if (evm.message.current_target, key) in evm.accessed_storage_keys: + charge_gas(evm, GasCosts.WARM_ACCESS) + else: + evm.accessed_storage_keys.add((evm.message.current_target, key)) + charge_gas(evm, GasCosts.COLD_STORAGE_ACCESS) + + # OPERATION + tx_state = evm.message.tx_env.state + value = get_storage(tx_state, evm.message.current_target, key) + + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def sstore(evm: Evm) -> None: + """ + Stores a value at a certain key in the current context's storage. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + if evm.message.is_static: + raise WriteInStaticContext + + # STACK + key = pop(evm.stack).to_be_bytes32() + new_value = pop(evm.stack) + + # check we have at least the stipend gas + check_gas(evm, GasCosts.CALL_STIPEND + Uint(1)) + + tx_state = evm.message.tx_env.state + original_value = get_storage_original( + tx_state, evm.message.current_target, key + ) + current_value = get_storage(tx_state, evm.message.current_target, key) + + gas_cost = Uint(0) + state_gas = StateGas(Uint(0)) + + # Access cost: cold or warm, always charged. + if (evm.message.current_target, key) not in evm.accessed_storage_keys: + evm.accessed_storage_keys.add((evm.message.current_target, key)) + gas_cost += GasCosts.COLD_STORAGE_ACCESS + else: + gas_cost += GasCosts.WARM_ACCESS + + # Write cost: charged on the first change to the slot this transaction. + if original_value == current_value and current_value != new_value: + gas_cost += GasCosts.STORAGE_WRITE + + # Refund Counter Calculation + if current_value != new_value: + if original_value != 0 and current_value != 0 and new_value == 0: + # Storage is cleared for the first time in the transaction + evm.refund_counter += GasCosts.REFUND_STORAGE_CLEAR + + if original_value != 0 and current_value == 0: + # Gas refund issued earlier to be reversed + evm.refund_counter -= GasCosts.REFUND_STORAGE_CLEAR + + if original_value == new_value: + # Slot restored to its original value: refund the STORAGE_WRITE + # charged on the first-time change earlier this transaction. + evm.refund_counter += int(GasCosts.STORAGE_WRITE) + + if original_value == current_value and current_value != new_value: + if original_value == 0: + state_gas = StateGasCosts.STORAGE_SET + + if current_value != new_value and original_value == new_value: + if original_value == 0: + # Slot set then cleared: refund the state gas charge. + credit_state_gas_refund(evm, StateGasCosts.STORAGE_SET) + + # Charge regular gas before state gas so that a regular-gas OOG + # does not consume state gas that would inflate the parent's + # reservoir on frame failure. + charge_gas(evm, gas_cost) + charge_state_gas(evm, state_gas) + set_storage(tx_state, evm.message.current_target, key, new_value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def tload(evm: Evm) -> None: + """ + Loads to the stack, the value corresponding to a certain key from the + transient storage of the current account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + + # GAS + charge_gas(evm, GasCosts.OPCODE_TLOAD) + + # OPERATION + value = get_transient_storage( + evm.message.tx_env.state, evm.message.current_target, key + ) + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def tstore(evm: Evm) -> None: + """ + Stores a value at a certain key in the current context's transient storage. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + if evm.message.is_static: + raise WriteInStaticContext + + # STACK + key = pop(evm.stack).to_be_bytes32() + new_value = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_TSTORE) + set_transient_storage( + evm.message.tx_env.state, + evm.message.current_target, + key, + new_value, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/bogota/vm/instructions/system.py b/src/ethereum/forks/bogota/vm/instructions/system.py new file mode 100644 index 00000000000..9d4e4fa815d --- /dev/null +++ b/src/ethereum/forks/bogota/vm/instructions/system.py @@ -0,0 +1,933 @@ +""" +Ethereum Virtual Machine (EVM) System Instructions. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM system related instructions. +""" + +from dataclasses import dataclass +from typing import final + +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import U256, Uint + +from ethereum.state import Address +from ethereum.utils.numeric import ceil32 + +from ...fork_types import StateGas +from ...state_tracker import ( + account_deployable, + get_account, + get_code, + increment_nonce, + is_account_alive, + move_ether, +) +from ...utils.address import ( + compute_contract_address, + compute_create2_contract_address, + to_address_masked, +) +from ...vm.eoa_delegation import ( + calculate_delegation_cost, +) +from .. import ( + CALL_SUCCESS, + Evm, + Message, + credit_state_gas_refund, + emit_transfer_log, + incorporate_child_on_error, + incorporate_child_on_success, +) +from ..exceptions import OutOfGasError, Revert, WriteInStaticContext +from ..gas import ( + GasCosts, + StateGasCosts, + calculate_gas_extend_memory, + calculate_message_call_gas, + charge_gas, + charge_state_gas, + check_gas, + init_code_cost, + max_message_call_gas, +) +from ..memory import memory_read_bytes, memory_write +from ..stack import pop, push + + +def generic_create( + evm: Evm, + endowment: U256, + contract_address: Address, + memory_start_position: U256, + memory_size: U256, +) -> None: + """ + Core logic used by the `CREATE*` family of opcodes. + """ + # This import causes a circular import error + # if it's not moved inside this method + from ...vm.interpreter import ( + MAX_INIT_CODE_SIZE, + STACK_DEPTH_LIMIT, + process_create_message, + ) + + # Check max init code size early before memory read + if memory_size > U256(MAX_INIT_CODE_SIZE): + raise OutOfGasError + + # Charge state gas for account creation (pay-before-execute). + # Refunded to the reservoir on any failure path below. + charge_state_gas(evm, StateGasCosts.NEW_ACCOUNT) + + tx_state = evm.message.tx_env.state + + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + + create_message_gas = max_message_call_gas(Uint(evm.gas_left)) + evm.gas_left -= create_message_gas + + # Move full reservoir to child (no 63/64 rule for state gas). Parent's + # `state_gas_left` is zeroed and restored when the child returns. + create_message_state_gas_reservoir = evm.state_gas_left + evm.state_gas_left = Uint(0) + + evm.return_data = b"" + + sender_address = evm.message.current_target + sender = get_account(tx_state, sender_address) + + if ( + sender.balance < endowment + or sender.nonce == Uint(2**64 - 1) + or evm.message.depth + Uint(1) > STACK_DEPTH_LIMIT + ): + evm.gas_left += create_message_gas + evm.state_gas_left += create_message_state_gas_reservoir + credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT) + push(evm.stack, U256(0)) + return + + evm.accessed_addresses.add(contract_address) + + if not account_deployable(tx_state, contract_address): + increment_nonce(tx_state, evm.message.current_target) + evm.regular_gas_used += create_message_gas + evm.state_gas_left += create_message_state_gas_reservoir + # Address collision — no account created, refund state gas. + credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT) + push(evm.stack, U256(0)) + return + + target_alive = is_account_alive(tx_state, contract_address) + + increment_nonce(tx_state, evm.message.current_target) + + child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, + caller=evm.message.current_target, + target=Bytes0(), + gas=create_message_gas, + state_gas_reservoir=create_message_state_gas_reservoir, + value=endowment, + data=b"", + code=call_data, + current_target=contract_address, + depth=evm.message.depth + Uint(1), + code_address=None, + should_transfer_value=True, + is_static=False, + accessed_addresses=evm.accessed_addresses.copy(), + accessed_storage_keys=evm.accessed_storage_keys.copy(), + disable_precompiles=False, + parent_evm=evm, + ) + child_evm = process_create_message(child_message) + + if child_evm.error: + incorporate_child_on_error(evm, child_evm) + # No account created, refund parent's CREATE state gas. + credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT) + evm.return_data = child_evm.output + push(evm.stack, U256(0)) + else: + incorporate_child_on_success(evm, child_evm) + if target_alive: + credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT) + evm.return_data = b"" + push(evm.stack, U256.from_be_bytes(child_evm.message.current_target)) + + +def create(evm: Evm) -> None: + """ + Creates a new account with associated code. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + if evm.message.is_static: + raise WriteInStaticContext + + # STACK + endowment = pop(evm.stack) + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + init_code_gas = init_code_cost(Uint(memory_size)) + charge_gas( + evm, + GasCosts.CREATE_ACCESS + extend_memory.cost + init_code_gas, + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + contract_address = compute_contract_address( + evm.message.current_target, + get_account( + evm.message.tx_env.state, evm.message.current_target + ).nonce, + ) + + generic_create( + evm, + endowment, + contract_address, + memory_start_position, + memory_size, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def create2(evm: Evm) -> None: + """ + Creates a new account with associated code. + + It's similar to the CREATE opcode except that the address of the new + account depends on the init_code instead of the nonce of sender. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + if evm.message.is_static: + raise WriteInStaticContext + + # STACK + endowment = pop(evm.stack) + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + salt = pop(evm.stack).to_be_bytes32() + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + call_data_words = ceil32(Uint(memory_size)) // Uint(32) + init_code_gas = init_code_cost(Uint(memory_size)) + charge_gas( + evm, + GasCosts.CREATE_ACCESS + + GasCosts.OPCODE_KECCAK256_PER_WORD * call_data_words + + extend_memory.cost + + init_code_gas, + ) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + contract_address = compute_create2_contract_address( + evm.message.current_target, + salt, + memory_read_bytes(evm.memory, memory_start_position, memory_size), + ) + + generic_create( + evm, + endowment, + contract_address, + memory_start_position, + memory_size, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def return_(evm: Evm) -> None: + """ + Halts execution returning output data. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + + charge_gas(evm, GasCosts.ZERO + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + evm.output = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + + evm.running = False + + # PROGRAM COUNTER + pass + + +@final +@dataclass +class GenericCall: + """ + Parameters for the core logic of the `CALL*` family of opcodes. + """ + + gas: Uint + state_gas_reservoir: Uint + value: U256 + caller: Address + to: Address + code_address: Address + should_transfer_value: bool + is_staticcall: bool + memory_input_start_position: U256 + memory_input_size: U256 + memory_output_start_position: U256 + memory_output_size: U256 + code: Bytes + disable_precompiles: bool + new_account_charged: bool = False + + +def generic_call(evm: Evm, params: GenericCall) -> None: + """ + Perform the core logic of the `CALL*` family of opcodes. + """ + from ...vm.interpreter import STACK_DEPTH_LIMIT, process_message + + evm.return_data = b"" + + if evm.message.depth + Uint(1) > STACK_DEPTH_LIMIT: + evm.gas_left += params.gas + evm.state_gas_left += params.state_gas_reservoir + if params.new_account_charged: + credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT) + push(evm.stack, U256(0)) + return + + call_data = memory_read_bytes( + evm.memory, + params.memory_input_start_position, + params.memory_input_size, + ) + + child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, + caller=params.caller, + target=params.to, + gas=params.gas, + state_gas_reservoir=params.state_gas_reservoir, + value=params.value, + data=call_data, + code=params.code, + current_target=params.to, + depth=evm.message.depth + Uint(1), + code_address=params.code_address, + should_transfer_value=params.should_transfer_value, + is_static=params.is_staticcall or evm.message.is_static, + accessed_addresses=evm.accessed_addresses.copy(), + accessed_storage_keys=evm.accessed_storage_keys.copy(), + disable_precompiles=params.disable_precompiles, + parent_evm=evm, + ) + + child_evm = process_message(child_message) + + if child_evm.error: + incorporate_child_on_error(evm, child_evm) + if params.new_account_charged: + credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT) + evm.return_data = child_evm.output + push(evm.stack, U256(0)) + else: + incorporate_child_on_success(evm, child_evm) + evm.return_data = child_evm.output + push(evm.stack, CALL_SUCCESS) + + actual_output_size = min( + params.memory_output_size, U256(len(child_evm.output)) + ) + memory_write( + evm.memory, + params.memory_output_start_position, + child_evm.output[:actual_output_size], + ) + + +def call(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + gas = Uint(pop(evm.stack)) + to = to_address_masked(pop(evm.stack)) + value = pop(evm.stack) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + if evm.message.is_static and value != U256(0): + raise WriteInStaticContext + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + is_cold_access = to not in evm.accessed_addresses + if is_cold_access: + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + else: + access_gas_cost = GasCosts.WARM_ACCESS + + transfer_gas_cost = Uint(0) if value == 0 else GasCosts.CALL_VALUE + + # check static gas before state access + check_gas( + evm, + access_gas_cost + transfer_gas_cost + extend_memory.cost, + ) + + # STATE ACCESS + tx_state = evm.message.tx_env.state + if is_cold_access: + evm.accessed_addresses.add(to) + + extra_gas = access_gas_cost + transfer_gas_cost + ( + is_delegated, + code_address, + delegation_access_cost, + ) = calculate_delegation_cost(evm, to) + + if is_delegated: + # check enough gas for delegation access + extra_gas += delegation_access_cost + check_gas(evm, extra_gas + extend_memory.cost) + if code_address not in evm.accessed_addresses: + evm.accessed_addresses.add(code_address) + + code_hash = get_account(tx_state, code_address).code_hash + code = get_code(tx_state, code_hash) + + charge_gas(evm, extra_gas + extend_memory.cost) + has_value = value != 0 + new_account_charged = has_value and not is_account_alive(tx_state, to) + if new_account_charged: + charge_state_gas(evm, StateGasCosts.NEW_ACCOUNT) + + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + memory_cost=Uint(0), + extra_gas=Uint(0), + ) + charge_gas(evm, message_call_gas.cost) + evm.regular_gas_used -= message_call_gas.sub_call + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + + # Pass full reservoir to child (no 63/64 rule for state gas) + call_state_gas_reservoir = evm.state_gas_left + evm.state_gas_left = Uint(0) + + sender_balance = get_account(tx_state, evm.message.current_target).balance + if sender_balance < value: + push(evm.stack, U256(0)) + evm.return_data = b"" + evm.gas_left += message_call_gas.sub_call + evm.state_gas_left += call_state_gas_reservoir + if new_account_charged: + credit_state_gas_refund(evm, StateGasCosts.NEW_ACCOUNT) + else: + generic_call( + evm, + GenericCall( + gas=message_call_gas.sub_call, + state_gas_reservoir=call_state_gas_reservoir, + value=value, + caller=evm.message.current_target, + to=to, + code_address=code_address, + should_transfer_value=True, + is_staticcall=False, + memory_input_start_position=memory_input_start_position, + memory_input_size=memory_input_size, + memory_output_start_position=memory_output_start_position, + memory_output_size=memory_output_size, + code=code, + disable_precompiles=is_delegated, + new_account_charged=new_account_charged, + ), + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def callcode(evm: Evm) -> None: + """ + Message-call into this account with alternative account's code. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + gas = Uint(pop(evm.stack)) + code_address = to_address_masked(pop(evm.stack)) + value = pop(evm.stack) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + to = evm.message.current_target + + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + is_cold_access = code_address not in evm.accessed_addresses + if is_cold_access: + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + else: + access_gas_cost = GasCosts.WARM_ACCESS + + transfer_gas_cost = Uint(0) if value == 0 else GasCosts.CALL_VALUE + + # check static gas before state access + check_gas( + evm, + access_gas_cost + extend_memory.cost + transfer_gas_cost, + ) + + # STATE ACCESS + tx_state = evm.message.tx_env.state + if is_cold_access: + evm.accessed_addresses.add(code_address) + + extra_gas = access_gas_cost + transfer_gas_cost + ( + is_delegated, + code_address, + delegation_access_cost, + ) = calculate_delegation_cost(evm, code_address) + + if is_delegated: + # check enough gas for delegation access + extra_gas += delegation_access_cost + check_gas(evm, extra_gas + extend_memory.cost) + if code_address not in evm.accessed_addresses: + evm.accessed_addresses.add(code_address) + + code_hash = get_account(tx_state, code_address).code_hash + code = get_code(tx_state, code_hash) + + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + extend_memory.cost, + extra_gas, + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + evm.regular_gas_used -= message_call_gas.sub_call + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + + # Pass full reservoir to child (no 63/64 rule for state gas) + call_state_gas_reservoir = evm.state_gas_left + evm.state_gas_left = Uint(0) + + sender_balance = get_account(tx_state, evm.message.current_target).balance + + if sender_balance < value: + push(evm.stack, U256(0)) + evm.return_data = b"" + evm.gas_left += message_call_gas.sub_call + evm.state_gas_left += call_state_gas_reservoir + else: + generic_call( + evm, + GenericCall( + gas=message_call_gas.sub_call, + state_gas_reservoir=call_state_gas_reservoir, + value=value, + caller=evm.message.current_target, + to=to, + code_address=code_address, + should_transfer_value=True, + is_staticcall=False, + memory_input_start_position=memory_input_start_position, + memory_input_size=memory_input_size, + memory_output_start_position=memory_output_start_position, + memory_output_size=memory_output_size, + code=code, + disable_precompiles=is_delegated, + ), + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def selfdestruct(evm: Evm) -> None: + """ + Halt execution and register account for later deletion. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + if evm.message.is_static: + raise WriteInStaticContext + + # STACK + beneficiary = to_address_masked(pop(evm.stack)) + + # GAS + gas_cost = GasCosts.OPCODE_SELFDESTRUCT_BASE + + is_cold_access = beneficiary not in evm.accessed_addresses + if is_cold_access: + gas_cost += GasCosts.COLD_ACCOUNT_ACCESS + + # check access gas cost before state access + check_gas(evm, gas_cost) + + # STATE ACCESS + tx_state = evm.message.tx_env.state + if is_cold_access: + evm.accessed_addresses.add(beneficiary) + + state_gas = StateGas(Uint(0)) + account_write_gas = Uint(0) + if ( + not is_account_alive(tx_state, beneficiary) + and get_account(tx_state, evm.message.current_target).balance != 0 + ): + state_gas = StateGasCosts.NEW_ACCOUNT + account_write_gas = GasCosts.ACCOUNT_WRITE + + # Charge regular gas before state gas so that a regular-gas OOG + # does not consume state gas that would inflate the parent's + # reservoir on frame failure. + charge_gas(evm, gas_cost + account_write_gas) + charge_state_gas(evm, state_gas) + + originator = evm.message.current_target + originator_balance = get_account(tx_state, originator).balance + + # Transfer balance + move_ether(tx_state, originator, beneficiary, originator_balance) + + # Emit transfer log + if beneficiary != originator: + emit_transfer_log(evm, originator, beneficiary, originator_balance) + + # Register account for deletion iff created in same transaction + if originator in tx_state.created_accounts: + evm.accounts_to_delete.add(originator) + + # HALT the execution + evm.running = False + + # PROGRAM COUNTER + pass + + +def delegatecall(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + gas = Uint(pop(evm.stack)) + code_address = to_address_masked(pop(evm.stack)) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + is_cold_access = code_address not in evm.accessed_addresses + if is_cold_access: + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + else: + access_gas_cost = GasCosts.WARM_ACCESS + + # check static gas before state access + check_gas(evm, access_gas_cost + extend_memory.cost) + + # STATE ACCESS + if is_cold_access: + evm.accessed_addresses.add(code_address) + + extra_gas = access_gas_cost + ( + is_delegated, + code_address, + delegation_access_cost, + ) = calculate_delegation_cost(evm, code_address) + + if is_delegated: + # check enough gas for delegation access + extra_gas += delegation_access_cost + check_gas(evm, extra_gas + extend_memory.cost) + if code_address not in evm.accessed_addresses: + evm.accessed_addresses.add(code_address) + + tx_state = evm.message.tx_env.state + code_hash = get_account(tx_state, code_address).code_hash + code = get_code(tx_state, code_hash) + + message_call_gas = calculate_message_call_gas( + U256(0), + gas, + Uint(evm.gas_left), + extend_memory.cost, + extra_gas, + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + evm.regular_gas_used -= message_call_gas.sub_call + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + + # Pass full reservoir to child (no 63/64 rule for state gas) + call_state_gas_reservoir = evm.state_gas_left + evm.state_gas_left = Uint(0) + + generic_call( + evm, + GenericCall( + gas=message_call_gas.sub_call, + state_gas_reservoir=call_state_gas_reservoir, + value=evm.message.value, + caller=evm.message.caller, + to=evm.message.current_target, + code_address=code_address, + should_transfer_value=False, + is_staticcall=False, + memory_input_start_position=memory_input_start_position, + memory_input_size=memory_input_size, + memory_output_start_position=memory_output_start_position, + memory_output_size=memory_output_size, + code=code, + disable_precompiles=is_delegated, + ), + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def staticcall(evm: Evm) -> None: + """ + Message-call into an account. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + gas = Uint(pop(evm.stack)) + to = to_address_masked(pop(evm.stack)) + memory_input_start_position = pop(evm.stack) + memory_input_size = pop(evm.stack) + memory_output_start_position = pop(evm.stack) + memory_output_size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + is_cold_access = to not in evm.accessed_addresses + if is_cold_access: + access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS + else: + access_gas_cost = GasCosts.WARM_ACCESS + + # check static gas before state access + check_gas(evm, access_gas_cost + extend_memory.cost) + + # STATE ACCESS + if is_cold_access: + evm.accessed_addresses.add(to) + + extra_gas = access_gas_cost + ( + is_delegated, + code_address, + delegation_access_cost, + ) = calculate_delegation_cost(evm, to) + + if is_delegated: + # check enough gas for delegation access + extra_gas += delegation_access_cost + check_gas(evm, extra_gas + extend_memory.cost) + if code_address not in evm.accessed_addresses: + evm.accessed_addresses.add(code_address) + + tx_state = evm.message.tx_env.state + code_hash = get_account(tx_state, code_address).code_hash + code = get_code(tx_state, code_hash) + + message_call_gas = calculate_message_call_gas( + U256(0), + gas, + Uint(evm.gas_left), + extend_memory.cost, + extra_gas, + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + evm.regular_gas_used -= message_call_gas.sub_call + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + + # Pass full reservoir to child (no 63/64 rule for state gas) + call_state_gas_reservoir = evm.state_gas_left + evm.state_gas_left = Uint(0) + + generic_call( + evm, + GenericCall( + gas=message_call_gas.sub_call, + state_gas_reservoir=call_state_gas_reservoir, + value=U256(0), + caller=evm.message.current_target, + to=to, + code_address=code_address, + should_transfer_value=True, + is_staticcall=True, + memory_input_start_position=memory_input_start_position, + memory_input_size=memory_input_size, + memory_output_start_position=memory_output_start_position, + memory_output_size=memory_output_size, + code=code, + disable_precompiles=is_delegated, + ), + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def revert(evm: Evm) -> None: + """ + Stop execution and revert state changes, without consuming all provided gas + and also has the ability to return a reason. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + size = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + + charge_gas(evm, extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + output = memory_read_bytes(evm.memory, memory_start_index, size) + evm.output = Bytes(output) + raise Revert + + # PROGRAM COUNTER + # no-op diff --git a/src/ethereum/forks/bogota/vm/interpreter.py b/src/ethereum/forks/bogota/vm/interpreter.py new file mode 100644 index 00000000000..9646bd0eec1 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/interpreter.py @@ -0,0 +1,375 @@ +""" +Ethereum Virtual Machine (EVM) Interpreter. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +A straightforward interpreter that executes EVM code. +""" + +from dataclasses import dataclass +from typing import Optional, Set, Tuple, final + +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import U256, Uint, ulen + +from ethereum.exceptions import EthereumException +from ethereum.state import Address +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) +from ethereum.utils.numeric import ceil32 + +from ..blocks import Log +from ..state_tracker import ( + account_deployable, + copy_tx_state, + destroy_storage, + get_account, + get_code, + increment_nonce, + is_account_alive, + mark_account_created, + move_ether, + restore_tx_state, + set_code, +) +from ..vm import Message +from ..vm.eoa_delegation import get_delegated_code_address, set_delegation +from ..vm.gas import ( + GasCosts, + StateGasCosts, + charge_gas, + charge_state_gas, +) +from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS +from . import ( + Evm, + emit_transfer_log, + frame_state_gas_used, + refill_frame_state_gas, +) +from .exceptions import ( + AddressCollision, + ExceptionalHalt, + InvalidContractPrefix, + InvalidOpcode, + OutOfGasError, + Revert, + StackDepthLimitError, +) +from .instructions import Ops, op_implementation +from .runtime import get_valid_jump_destinations + +STACK_DEPTH_LIMIT = Uint(1024) +MAX_CODE_SIZE = 0x10000 +MAX_INIT_CODE_SIZE = 2 * MAX_CODE_SIZE + + +@final +@dataclass +class MessageCallOutput: + """ + Output of a particular message call. + + Contains the following: + + 1. `gas_left`: remaining gas after execution. + 2. `refund_counter`: gas to refund after execution. + 3. `logs`: list of `Log` generated during execution. + 4. `accounts_to_delete`: Contracts which have self-destructed. + 5. `error`: The error from the execution if any. + 6. `return_data`: The output of the execution. + 7. `regular_gas_used`: Regular gas used during execution. + 8. `state_gas_used`: State gas used during execution. + 9. `state_refund`: State gas refunded by `set_delegation` for + authorities that already existed in state. Subtracted from + `tx_state_gas` in block accounting so `block.gas_used` + matches the receipt `cumulative_gas_used`. + 10. `created_target_alive`: Whether a top-level creation + transaction targeted an already-existent account. + """ + + gas_left: Uint + refund_counter: U256 + logs: Tuple[Log, ...] + accounts_to_delete: Set[Address] + error: Optional[EthereumException] + return_data: Bytes + state_gas_left: Uint + regular_gas_used: Uint + state_gas_used: int + state_refund: Uint + created_target_alive: bool + + +def process_message_call(message: Message) -> MessageCallOutput: + """ + If `message.target` is empty then it creates a smart contract + else it executes a call from the `message.caller` to the `message.target`. + + Parameters + ---------- + message : + Transaction specific items. + + Returns + ------- + output : `MessageCallOutput` + Output of the message call + + """ + tx_state = message.tx_env.state + refund_counter = U256(0) + state_refund = Uint(0) + target_alive = False + if message.target == Bytes0(b""): + if account_deployable(tx_state, message.current_target): + target_alive = is_account_alive(tx_state, message.current_target) + evm = process_create_message(message) + else: + return MessageCallOutput( + gas_left=Uint(0), + refund_counter=U256(0), + logs=tuple(), + accounts_to_delete=set(), + error=AddressCollision(), + return_data=Bytes(b""), + state_gas_left=message.state_gas_reservoir, + regular_gas_used=message.gas, + state_gas_used=0, + state_refund=Uint(0), + created_target_alive=False, + ) + else: + if message.tx_env.authorizations != (): + auth_state_refund, auth_regular_refund = set_delegation(message) + state_refund += auth_state_refund + refund_counter += U256(auth_regular_refund) + + delegated_address = get_delegated_code_address(message.code) + if delegated_address is not None: + message.disable_precompiles = True + message.code = get_code( + tx_state, + get_account(tx_state, delegated_address).code_hash, + ) + message.code_address = delegated_address + + evm = process_message(message) + + if evm.error: + logs: Tuple[Log, ...] = () + accounts_to_delete = set() + else: + logs = evm.logs + accounts_to_delete = evm.accounts_to_delete + refund_counter += U256(evm.refund_counter) + + tx_end = TransactionEnd( + int(message.gas) - int(evm.gas_left), evm.output, evm.error + ) + evm_trace(evm, tx_end) + + return MessageCallOutput( + gas_left=evm.gas_left, + refund_counter=refund_counter, + logs=logs, + accounts_to_delete=accounts_to_delete, + error=evm.error, + return_data=evm.output, + state_gas_left=evm.state_gas_left, + regular_gas_used=evm.regular_gas_used, + state_gas_used=frame_state_gas_used(evm), + state_refund=state_refund, + created_target_alive=target_alive, + ) + + +def process_create_message(message: Message) -> Evm: + """ + Executes a call to create a smart contract. + + Parameters + ---------- + message : + Transaction specific items. + + Returns + ------- + evm: :py:class:`~ethereum.forks.bogota.vm.Evm` + Items containing execution specific objects. + + """ + tx_state = message.tx_env.state + # take snapshot of state before processing the message + snapshot = copy_tx_state(tx_state) + + # If the address where the account is being created has storage, it is + # destroyed. This can only happen in the following highly unlikely + # circumstances: + # * The address created by a `CREATE` call collides with a subsequent + # `CREATE` or `CREATE2` call. + # * The first `CREATE` happened before Spurious Dragon and left empty + # code. + destroy_storage(tx_state, message.current_target) + + # In the previously mentioned edge case the preexisting storage is ignored + # for gas refund purposes. In order to do this we must track created + # accounts. This tracking is also needed to respect the constraints + # added to SELFDESTRUCT by EIP-6780. + mark_account_created(tx_state, message.current_target) + + increment_nonce(tx_state, message.current_target) + + evm = process_message(message) + if not evm.error: + contract_code = evm.output + try: + if len(contract_code) > 0: + if contract_code[0] == 0xEF: + raise InvalidContractPrefix + if len(contract_code) > MAX_CODE_SIZE: + raise OutOfGasError + # Hash cost for computing keccak256 of deployed bytecode + code_hash_gas = ( + GasCosts.OPCODE_KECCAK256_PER_WORD + * ceil32(ulen(contract_code)) + // Uint(32) + ) + charge_gas(evm, code_hash_gas) + code_deposit_state_gas = ( + ulen(contract_code) * StateGasCosts.COST_PER_STATE_BYTE + ) + charge_state_gas(evm, code_deposit_state_gas) + except ExceptionalHalt as error: + restore_tx_state(tx_state, snapshot) + refill_frame_state_gas(evm) + evm.regular_gas_used += evm.gas_left + evm.gas_left = Uint(0) + evm.output = b"" + evm.error = error + else: + set_code(tx_state, message.current_target, contract_code) + else: + restore_tx_state(tx_state, snapshot) + return evm + + +def process_message(message: Message) -> Evm: + """ + Move ether and execute the relevant code. + + Parameters + ---------- + message : + Transaction specific items. + + Returns + ------- + evm: :py:class:`~ethereum.forks.bogota.vm.Evm` + Items containing execution specific objects + + """ + tx_state = message.tx_env.state + if message.depth > STACK_DEPTH_LIMIT: + raise StackDepthLimitError("Stack depth limit reached") + + code = message.code + valid_jump_destinations = get_valid_jump_destinations(code) + evm = Evm( + pc=Uint(0), + stack=[], + memory=bytearray(), + code=code, + gas_left=message.gas, + state_gas_left=message.state_gas_reservoir, + valid_jump_destinations=valid_jump_destinations, + logs=(), + refund_counter=0, + running=True, + message=message, + output=b"", + accounts_to_delete=set(), + return_data=b"", + error=None, + accessed_addresses=message.accessed_addresses, + accessed_storage_keys=message.accessed_storage_keys, + ) + + snapshot = copy_tx_state(tx_state) + + # Execute message code and handle errors + try: + if message.depth == Uint(0) and message.target != Bytes0(b""): + recipient = message.current_target + if message.value > U256(0) and not is_account_alive( + tx_state, recipient + ): + charge_state_gas(evm, StateGasCosts.NEW_ACCOUNT) + recipient_code = get_code( + tx_state, get_account(tx_state, recipient).code_hash + ) + delegated_address = get_delegated_code_address(recipient_code) + if delegated_address is not None: + charge_gas(evm, GasCosts.COLD_ACCOUNT_ACCESS) + evm.accessed_addresses.add(delegated_address) + + if message.should_transfer_value and message.value != 0: + move_ether( + tx_state, + message.caller, + message.current_target, + message.value, + ) + if message.caller != message.current_target: + emit_transfer_log( + evm, + message.caller, + message.current_target, + message.value, + ) + if evm.message.code_address in PRE_COMPILED_CONTRACTS: + if not message.disable_precompiles: + evm_trace(evm, PrecompileStart(evm.message.code_address)) + PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) + else: + while evm.running and evm.pc < ulen(evm.code): + try: + op = Ops(evm.code[evm.pc]) + except ValueError as e: + raise InvalidOpcode(evm.code[evm.pc]) from e + + evm_trace(evm, OpStart(op)) + op_implementation[op](evm) + evm_trace(evm, OpEnd()) + + evm_trace(evm, EvmStop(Ops.STOP)) + + except ExceptionalHalt as error: + evm_trace(evm, OpException(error)) + refill_frame_state_gas(evm) + evm.regular_gas_used += evm.gas_left + evm.gas_left = Uint(0) + evm.output = b"" + evm.error = error + except Revert as error: + evm_trace(evm, OpException(error)) + refill_frame_state_gas(evm) + evm.error = error + + if evm.error: + restore_tx_state(tx_state, snapshot) + return evm diff --git a/src/ethereum/forks/bogota/vm/memory.py b/src/ethereum/forks/bogota/vm/memory.py new file mode 100644 index 00000000000..3b76b2454c6 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/memory.py @@ -0,0 +1,83 @@ +""" +Ethereum Virtual Machine (EVM) Memory. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +EVM memory operations. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.byte import right_pad_zero_bytes + + +def memory_write( + memory: bytearray, start_position: U256, value: Bytes +) -> None: + """ + Writes to memory. + + Parameters + ---------- + memory : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + value : + Data to write to memory. + + """ + memory[start_position : int(start_position) + len(value)] = value + + +def memory_read_bytes( + memory: bytearray, start_position: U256, size: U256 +) -> Bytes: + """ + Read bytes from memory. + + Parameters + ---------- + memory : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + size : + Size of the data that needs to be read from `start_position`. + + Returns + ------- + data_bytes : + Data read from memory. + + """ + return Bytes(memory[start_position : Uint(start_position) + Uint(size)]) + + +def buffer_read(buffer: Bytes, start_position: U256, size: U256) -> Bytes: + """ + Read bytes from a buffer. Padding with zeros if necessary. + + Parameters + ---------- + buffer : + Memory contents of the EVM. + start_position : + Starting pointer to the memory. + size : + Size of the data that needs to be read from `start_position`. + + Returns + ------- + data_bytes : + Data read from memory. + + """ + buffer_slice = buffer[start_position : Uint(start_position) + Uint(size)] + return right_pad_zero_bytes(bytes(buffer_slice), size) diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/__init__.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/__init__.py new file mode 100644 index 00000000000..d32959fc937 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/__init__.py @@ -0,0 +1,55 @@ +""" +Precompiled Contract Addresses. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Addresses of precompiled contracts and mappings to their +implementations. +""" + +from ...utils.hexadecimal import hex_to_address + +__all__ = ( + "ECRECOVER_ADDRESS", + "SHA256_ADDRESS", + "RIPEMD160_ADDRESS", + "IDENTITY_ADDRESS", + "MODEXP_ADDRESS", + "ALT_BN128_ADD_ADDRESS", + "ALT_BN128_MUL_ADDRESS", + "ALT_BN128_PAIRING_CHECK_ADDRESS", + "BLAKE2F_ADDRESS", + "POINT_EVALUATION_ADDRESS", + "BLS12_G1_ADD_ADDRESS", + "BLS12_G1_MSM_ADDRESS", + "BLS12_G2_ADD_ADDRESS", + "BLS12_G2_MSM_ADDRESS", + "BLS12_PAIRING_ADDRESS", + "BLS12_MAP_FP_TO_G1_ADDRESS", + "BLS12_MAP_FP2_TO_G2_ADDRESS", + "P256VERIFY_ADDRESS", +) + +ECRECOVER_ADDRESS = hex_to_address("0x01") +SHA256_ADDRESS = hex_to_address("0x02") +RIPEMD160_ADDRESS = hex_to_address("0x03") +IDENTITY_ADDRESS = hex_to_address("0x04") +MODEXP_ADDRESS = hex_to_address("0x05") +ALT_BN128_ADD_ADDRESS = hex_to_address("0x06") +ALT_BN128_MUL_ADDRESS = hex_to_address("0x07") +ALT_BN128_PAIRING_CHECK_ADDRESS = hex_to_address("0x08") +BLAKE2F_ADDRESS = hex_to_address("0x09") +POINT_EVALUATION_ADDRESS = hex_to_address("0x0a") +BLS12_G1_ADD_ADDRESS = hex_to_address("0x0b") +BLS12_G1_MSM_ADDRESS = hex_to_address("0x0c") +BLS12_G2_ADD_ADDRESS = hex_to_address("0x0d") +BLS12_G2_MSM_ADDRESS = hex_to_address("0x0e") +BLS12_PAIRING_ADDRESS = hex_to_address("0x0f") +BLS12_MAP_FP_TO_G1_ADDRESS = hex_to_address("0x10") +BLS12_MAP_FP2_TO_G2_ADDRESS = hex_to_address("0x11") +P256VERIFY_ADDRESS = hex_to_address("0x100") diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/alt_bn128.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/alt_bn128.py new file mode 100644 index 00000000000..862506c54c3 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/alt_bn128.py @@ -0,0 +1,234 @@ +""" +Ethereum Virtual Machine (EVM) ALT_BN128 CONTRACTS. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the ALT_BN128 precompiled contracts. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint, ulen +from py_ecc.optimized_bn128.optimized_curve import ( + FQ, + FQ2, + FQ12, + add, + b, + b2, + curve_order, + field_modulus, + is_inf, + is_on_curve, + multiply, + normalize, +) +from py_ecc.optimized_bn128.optimized_pairing import pairing +from py_ecc.typing import Optimized_Point3D as Point3D + +from ...vm import Evm +from ...vm.gas import GasCosts, charge_gas +from ...vm.memory import buffer_read +from ..exceptions import InvalidParameter, OutOfGasError + + +def bytes_to_g1(data: Bytes) -> Point3D[FQ]: + """ + Decode 64 bytes to a point on the curve. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Point3D + A point on the curve. + + Raises + ------ + InvalidParameter + Either a field element is invalid or the point is not on the curve. + + """ + if len(data) != 64: + raise InvalidParameter("Input should be 64 bytes long") + + x_bytes = buffer_read(data, U256(0), U256(32)) + x = int(U256.from_be_bytes(x_bytes)) + y_bytes = buffer_read(data, U256(32), U256(32)) + y = int(U256.from_be_bytes(y_bytes)) + + if x >= field_modulus: + raise InvalidParameter("Invalid field element") + if y >= field_modulus: + raise InvalidParameter("Invalid field element") + + z = 1 + if x == 0 and y == 0: + z = 0 + + point = (FQ(x), FQ(y), FQ(z)) + + # Check if the point is on the curve + if not is_on_curve(point, b): + raise InvalidParameter("Point is not on curve") + + return point + + +def bytes_to_g2(data: Bytes) -> Point3D[FQ2]: + """ + Decode 128 bytes to a G2 point. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Point2D + A point on the curve. + + Raises + ------ + InvalidParameter + Either a field element is invalid or the point is not on the curve. + + """ + if len(data) != 128: + raise InvalidParameter("G2 should be 128 bytes long") + + x0_bytes = buffer_read(data, U256(0), U256(32)) + x0 = int(U256.from_be_bytes(x0_bytes)) + x1_bytes = buffer_read(data, U256(32), U256(32)) + x1 = int(U256.from_be_bytes(x1_bytes)) + + y0_bytes = buffer_read(data, U256(64), U256(32)) + y0 = int(U256.from_be_bytes(y0_bytes)) + y1_bytes = buffer_read(data, U256(96), U256(32)) + y1 = int(U256.from_be_bytes(y1_bytes)) + + if x0 >= field_modulus or x1 >= field_modulus: + raise InvalidParameter("Invalid field element") + if y0 >= field_modulus or y1 >= field_modulus: + raise InvalidParameter("Invalid field element") + + x = FQ2((x1, x0)) + y = FQ2((y1, y0)) + + z = (1, 0) + if x == FQ2((0, 0)) and y == FQ2((0, 0)): + z = (0, 0) + + point = (x, y, FQ2(z)) + + # Check if the point is on the curve + if not is_on_curve(point, b2): + raise InvalidParameter("Point is not on curve") + + return point + + +def alt_bn128_add(evm: Evm) -> None: + """ + The ALT_BN128 addition precompiled contract. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_ECADD) + + # OPERATION + try: + p0 = bytes_to_g1(buffer_read(data, U256(0), U256(64))) + p1 = bytes_to_g1(buffer_read(data, U256(64), U256(64))) + except InvalidParameter as e: + raise OutOfGasError from e + + p = add(p0, p1) + x, y = normalize(p) + + evm.output = Uint(x).to_be_bytes32() + Uint(y).to_be_bytes32() + + +def alt_bn128_mul(evm: Evm) -> None: + """ + The ALT_BN128 multiplication precompiled contract. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_ECMUL) + + # OPERATION + try: + p0 = bytes_to_g1(buffer_read(data, U256(0), U256(64))) + except InvalidParameter as e: + raise OutOfGasError from e + n = int(U256.from_be_bytes(buffer_read(data, U256(64), U256(32)))) + + p = multiply(p0, n) + x, y = normalize(p) + + evm.output = Uint(x).to_be_bytes32() + Uint(y).to_be_bytes32() + + +def alt_bn128_pairing_check(evm: Evm) -> None: + """ + The ALT_BN128 pairing check precompiled contract. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas( + evm, + GasCosts.PRECOMPILE_ECPAIRING_PER_POINT * (ulen(data) // Uint(192)) + + GasCosts.PRECOMPILE_ECPAIRING_BASE, + ) + + # OPERATION + if len(data) % 192 != 0: + raise OutOfGasError + result = FQ12.one() + for i in range(len(data) // 192): + try: + p = bytes_to_g1(buffer_read(data, U256(192 * i), U256(64))) + q = bytes_to_g2(buffer_read(data, U256(192 * i + 64), U256(128))) + except InvalidParameter as e: + raise OutOfGasError from e + if not is_inf(multiply(p, curve_order)): + raise OutOfGasError + if not is_inf(multiply(q, curve_order)): + raise OutOfGasError + + result *= pairing(q, p) + + if result == FQ12.one(): + evm.output = U256(1).to_be_bytes32() + else: + evm.output = U256(0).to_be_bytes32() diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/blake2f.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/blake2f.py new file mode 100644 index 00000000000..ae53b1ab4b5 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/blake2f.py @@ -0,0 +1,42 @@ +""" +Ethereum Virtual Machine (EVM) Blake2 PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `Blake2` precompiled contract. +""" + +from ethereum.crypto.blake2 import Blake2b + +from ...vm import Evm +from ...vm.gas import GasCosts, charge_gas +from ..exceptions import InvalidParameter + + +def blake2f(evm: Evm) -> None: + """ + Writes the Blake2 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + if len(data) != 213: + raise InvalidParameter + + blake2b = Blake2b() + rounds, h, m, t_0, t_1, f = blake2b.get_blake2_parameters(data) + + charge_gas(evm, GasCosts.PRECOMPILE_BLAKE2F_PER_ROUND * rounds) + if f not in [0, 1]: + raise InvalidParameter + + evm.output = blake2b.compress(rounds, h, m, t_0, t_1, f) diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/__init__.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/__init__.py new file mode 100644 index 00000000000..7d622da39d1 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/__init__.py @@ -0,0 +1,622 @@ +""" +BLS12 381 Precompile. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Precompile for BLS12-381 curve operations. +""" + +from functools import lru_cache +from typing import Tuple + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint +from py_ecc.optimized_bls12_381.optimized_curve import ( + FQ, + FQ2, + b, + b2, + curve_order, + is_inf, + is_on_curve, + normalize, +) +from py_ecc.optimized_bls12_381.optimized_curve import ( + multiply as bls12_multiply, +) +from py_ecc.typing import Optimized_Point3D as Point3D + +from ....vm.memory import buffer_read +from ...exceptions import InvalidParameter + +G1_K_DISCOUNT = [ + 1000, + 949, + 848, + 797, + 764, + 750, + 738, + 728, + 719, + 712, + 705, + 698, + 692, + 687, + 682, + 677, + 673, + 669, + 665, + 661, + 658, + 654, + 651, + 648, + 645, + 642, + 640, + 637, + 635, + 632, + 630, + 627, + 625, + 623, + 621, + 619, + 617, + 615, + 613, + 611, + 609, + 608, + 606, + 604, + 603, + 601, + 599, + 598, + 596, + 595, + 593, + 592, + 591, + 589, + 588, + 586, + 585, + 584, + 582, + 581, + 580, + 579, + 577, + 576, + 575, + 574, + 573, + 572, + 570, + 569, + 568, + 567, + 566, + 565, + 564, + 563, + 562, + 561, + 560, + 559, + 558, + 557, + 556, + 555, + 554, + 553, + 552, + 551, + 550, + 549, + 548, + 547, + 547, + 546, + 545, + 544, + 543, + 542, + 541, + 540, + 540, + 539, + 538, + 537, + 536, + 536, + 535, + 534, + 533, + 532, + 532, + 531, + 530, + 529, + 528, + 528, + 527, + 526, + 525, + 525, + 524, + 523, + 522, + 522, + 521, + 520, + 520, + 519, +] + +G2_K_DISCOUNT = [ + 1000, + 1000, + 923, + 884, + 855, + 832, + 812, + 796, + 782, + 770, + 759, + 749, + 740, + 732, + 724, + 717, + 711, + 704, + 699, + 693, + 688, + 683, + 679, + 674, + 670, + 666, + 663, + 659, + 655, + 652, + 649, + 646, + 643, + 640, + 637, + 634, + 632, + 629, + 627, + 624, + 622, + 620, + 618, + 615, + 613, + 611, + 609, + 607, + 606, + 604, + 602, + 600, + 598, + 597, + 595, + 593, + 592, + 590, + 589, + 587, + 586, + 584, + 583, + 582, + 580, + 579, + 578, + 576, + 575, + 574, + 573, + 571, + 570, + 569, + 568, + 567, + 566, + 565, + 563, + 562, + 561, + 560, + 559, + 558, + 557, + 556, + 555, + 554, + 553, + 552, + 552, + 551, + 550, + 549, + 548, + 547, + 546, + 545, + 545, + 544, + 543, + 542, + 541, + 541, + 540, + 539, + 538, + 537, + 537, + 536, + 535, + 535, + 534, + 533, + 532, + 532, + 531, + 530, + 530, + 529, + 528, + 528, + 527, + 526, + 526, + 525, + 524, + 524, +] + +G1_MAX_DISCOUNT = 519 +G2_MAX_DISCOUNT = 524 +MULTIPLIER = Uint(1000) + + +# Note: Caching as a way to optimize client performance can create a DoS +# attack vector for worst-case inputs that trigger only cache misses. This +# should not be relied upon for client performance optimization in +# production systems. +@lru_cache(maxsize=128) +def _bytes_to_g1_cached( + data: bytes, + subgroup_check: bool = False, +) -> Point3D[FQ]: + """ + Internal cached version of `bytes_to_g1` that works with hashable `bytes`. + """ + if len(data) != 128: + raise InvalidParameter("Input should be 128 bytes long") + + x = bytes_to_fq(data[:64]) + y = bytes_to_fq(data[64:]) + + if x >= FQ.field_modulus: + raise InvalidParameter("x >= field modulus") + if y >= FQ.field_modulus: + raise InvalidParameter("y >= field modulus") + + z = 1 + if x == 0 and y == 0: + z = 0 + point = FQ(x), FQ(y), FQ(z) + + if not is_on_curve(point, b): + raise InvalidParameter("G1 point is not on curve") + + if subgroup_check and not is_inf(bls12_multiply(point, curve_order)): + raise InvalidParameter("Subgroup check failed for G1 point.") + + return point + + +def bytes_to_g1( + data: Bytes, + subgroup_check: bool = False, +) -> Point3D[FQ]: + """ + Decode 128 bytes to a G1 point with or without subgroup check. + + Parameters + ---------- + data : + The bytes data to decode. + subgroup_check : bool + Whether to perform a subgroup check on the G1 point. + + Returns + ------- + point : Point3D[FQ] + The G1 point. + + Raises + ------ + InvalidParameter + If a field element is invalid, the point is not on the curve, or the + subgroup check fails. + + """ + # This is needed bc when we slice `Bytes` we get a `bytearray`, + # which is not hashable + return _bytes_to_g1_cached(bytes(data), subgroup_check) + + +def g1_to_bytes( + g1_point: Point3D[FQ], +) -> Bytes: + """ + Encode a G1 point to 128 bytes. + + Parameters + ---------- + g1_point : + The G1 point to encode. + + Returns + ------- + data : Bytes + The encoded data. + + """ + g1_normalized = normalize(g1_point) + x, y = g1_normalized + return int(x).to_bytes(64, "big") + int(y).to_bytes(64, "big") + + +def decode_g1_scalar_pair( + data: Bytes, +) -> Tuple[Point3D[FQ], int]: + """ + Decode 160 bytes to a G1 point and a scalar. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Tuple[Point3D[FQ], int] + The G1 point and the scalar. + + Raises + ------ + InvalidParameter + If the subgroup check failed. + + """ + if len(data) != 160: + raise InvalidParameter("Input should be 160 bytes long") + + point = bytes_to_g1(data[:128], subgroup_check=True) + + m = int.from_bytes(buffer_read(data, U256(128), U256(32)), "big") + + return point, m + + +def bytes_to_fq(data: Bytes) -> FQ: + """ + Decode 64 bytes to a FQ element. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + fq : FQ + The FQ element. + + Raises + ------ + InvalidParameter + If the field element is invalid. + + """ + if len(data) != 64: + raise InvalidParameter("FQ should be 64 bytes long") + + c = int.from_bytes(data[:64], "big") + + if c >= FQ.field_modulus: + raise InvalidParameter("Invalid field element") + + return FQ(c) + + +def bytes_to_fq2(data: Bytes) -> FQ2: + """ + Decode 128 bytes to an FQ2 element. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + fq2 : FQ2 + The FQ2 element. + + Raises + ------ + InvalidParameter + If the field element is invalid. + + """ + if len(data) != 128: + raise InvalidParameter("FQ2 input should be 128 bytes long") + c_0 = int.from_bytes(data[:64], "big") + c_1 = int.from_bytes(data[64:], "big") + + if c_0 >= FQ.field_modulus: + raise InvalidParameter("Invalid field element") + if c_1 >= FQ.field_modulus: + raise InvalidParameter("Invalid field element") + + return FQ2((c_0, c_1)) + + +# Note: Caching as a way to optimize client performance can create a DoS +# attack vector for worst-case inputs that trigger only cache misses. This +# should not be relied upon for client performance optimization in +# production systems. +@lru_cache(maxsize=128) +def _bytes_to_g2_cached( + data: bytes, + subgroup_check: bool = False, +) -> Point3D[FQ2]: + """ + Internal cached version of `bytes_to_g2` that works with hashable `bytes`. + """ + if len(data) != 256: + raise InvalidParameter("G2 should be 256 bytes long") + + x = bytes_to_fq2(data[:128]) + y = bytes_to_fq2(data[128:]) + + z = (1, 0) + if x == FQ2((0, 0)) and y == FQ2((0, 0)): + z = (0, 0) + + point = x, y, FQ2(z) + + if not is_on_curve(point, b2): + raise InvalidParameter("Point is not on curve") + + if subgroup_check and not is_inf(bls12_multiply(point, curve_order)): + raise InvalidParameter("Subgroup check failed for G2 point.") + + return point + + +def bytes_to_g2( + data: Bytes, + subgroup_check: bool = False, +) -> Point3D[FQ2]: + """ + Decode 256 bytes to a G2 point with or without subgroup check. + + Parameters + ---------- + data : + The bytes data to decode. + subgroup_check : bool + Whether to perform a subgroup check on the G2 point. + + Returns + ------- + point : Point3D[FQ2] + The G2 point. + + Raises + ------ + InvalidParameter + If a field element is invalid, the point is not on the curve, or the + subgroup check fails. + + """ + # This is needed bc when we slice `Bytes` we get a `bytearray`, + # which is not hashable + return _bytes_to_g2_cached(data, subgroup_check) + + +def fq2_to_bytes(fq2: FQ2) -> Bytes: + """ + Encode a FQ2 point to 128 bytes. + + Parameters + ---------- + fq2 : + The FQ2 point to encode. + + Returns + ------- + data : Bytes + The encoded data. + + """ + coord0, coord1 = fq2.coeffs + return int(coord0).to_bytes(64, "big") + int(coord1).to_bytes(64, "big") + + +def g2_to_bytes( + g2_point: Point3D[FQ2], +) -> Bytes: + """ + Encode a G2 point to 256 bytes. + + Parameters + ---------- + g2_point : + The G2 point to encode. + + Returns + ------- + data : Bytes + The encoded data. + + """ + x_coords, y_coords = normalize(g2_point) + return fq2_to_bytes(x_coords) + fq2_to_bytes(y_coords) + + +def decode_g2_scalar_pair( + data: Bytes, +) -> Tuple[Point3D[FQ2], int]: + """ + Decode 288 bytes to a G2 point and a scalar. + + Parameters + ---------- + data : + The bytes data to decode. + + Returns + ------- + point : Tuple[Point3D[FQ2], int] + The G2 point and the scalar. + + Raises + ------ + InvalidParameter + If the subgroup check failed. + + """ + if len(data) != 288: + raise InvalidParameter("Input should be 288 bytes long") + + point = bytes_to_g2(data[:256], subgroup_check=True) + n = int.from_bytes(data[256 : 256 + 32], "big") + + return point, n diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/bls12_381_g1.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/bls12_381_g1.py new file mode 100644 index 00000000000..d1f63224a0c --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/bls12_381_g1.py @@ -0,0 +1,149 @@ +""" +Ethereum Virtual Machine (EVM) BLS12 381 CONTRACTS. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of pre-compiles in G1 (curve over base prime field). +""" + +from ethereum_types.numeric import U256, Uint +from py_ecc.bls.hash_to_curve import clear_cofactor_G1, map_to_curve_G1 +from py_ecc.optimized_bls12_381.optimized_curve import FQ +from py_ecc.optimized_bls12_381.optimized_curve import add as bls12_add +from py_ecc.optimized_bls12_381.optimized_curve import ( + multiply as bls12_multiply, +) + +from ....vm import Evm +from ....vm.gas import ( + GasCosts, + charge_gas, +) +from ....vm.memory import buffer_read +from ...exceptions import InvalidParameter +from . import ( + G1_K_DISCOUNT, + G1_MAX_DISCOUNT, + MULTIPLIER, + bytes_to_g1, + decode_g1_scalar_pair, + g1_to_bytes, +) + +LENGTH_PER_PAIR = 160 + + +def bls12_g1_add(evm: Evm) -> None: + """ + The bls12_381 G1 point addition precompile. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) != 256: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_BLS_G1ADD) + + # OPERATION + p1 = bytes_to_g1(buffer_read(data, U256(0), U256(128))) + p2 = bytes_to_g1(buffer_read(data, U256(128), U256(128))) + + result = bls12_add(p1, p2) + + evm.output = g1_to_bytes(result) + + +def bls12_g1_msm(evm: Evm) -> None: + """ + The bls12_381 G1 multi-scalar multiplication precompile. + Note: This uses the naive approach to multi-scalar multiplication + which is not suitably optimized for production clients. Clients are + required to implement a more efficient algorithm such as the Pippenger + algorithm. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) == 0 or len(data) % LENGTH_PER_PAIR != 0: + raise InvalidParameter("Invalid Input Length") + + # GAS + k = len(data) // LENGTH_PER_PAIR + if k <= 128: + discount = Uint(G1_K_DISCOUNT[k - 1]) + else: + discount = Uint(G1_MAX_DISCOUNT) + + gas_cost = Uint(k) * GasCosts.PRECOMPILE_BLS_G1MUL * discount // MULTIPLIER + charge_gas(evm, gas_cost) + + # OPERATION + for i in range(k): + start_index = i * LENGTH_PER_PAIR + end_index = start_index + LENGTH_PER_PAIR + + p, m = decode_g1_scalar_pair(data[start_index:end_index]) + product = bls12_multiply(p, m) + + if i == 0: + result = product + else: + result = bls12_add(result, product) + + evm.output = g1_to_bytes(result) + + +def bls12_map_fp_to_g1(evm: Evm) -> None: + """ + Precompile to map field element to G1. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) != 64: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_BLS_G1MAP) + + # OPERATION + fp = int.from_bytes(data, "big") + if fp >= FQ.field_modulus: + raise InvalidParameter("coordinate >= field modulus") + + g1_optimized_3d = clear_cofactor_G1(map_to_curve_G1(FQ(fp))) + evm.output = g1_to_bytes(g1_optimized_3d) diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/bls12_381_g2.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/bls12_381_g2.py new file mode 100644 index 00000000000..2fd32313f89 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/bls12_381_g2.py @@ -0,0 +1,151 @@ +""" +Ethereum Virtual Machine (EVM) BLS12 381 G2 CONTRACTS. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of pre-compiles in G2 (curve over base prime field). +""" + +from ethereum_types.numeric import U256, Uint +from py_ecc.bls.hash_to_curve import clear_cofactor_G2, map_to_curve_G2 +from py_ecc.optimized_bls12_381.optimized_curve import FQ2 +from py_ecc.optimized_bls12_381.optimized_curve import add as bls12_add +from py_ecc.optimized_bls12_381.optimized_curve import ( + multiply as bls12_multiply, +) + +from ....vm import Evm +from ....vm.gas import ( + GasCosts, + charge_gas, +) +from ....vm.memory import buffer_read +from ...exceptions import InvalidParameter +from . import ( + G2_K_DISCOUNT, + G2_MAX_DISCOUNT, + MULTIPLIER, + bytes_to_fq2, + bytes_to_g2, + decode_g2_scalar_pair, + g2_to_bytes, +) + +LENGTH_PER_PAIR = 288 + + +def bls12_g2_add(evm: Evm) -> None: + """ + The bls12_381 G2 point addition precompile. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) != 512: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_BLS_G2ADD) + + # OPERATION + p1 = bytes_to_g2(buffer_read(data, U256(0), U256(256))) + p2 = bytes_to_g2(buffer_read(data, U256(256), U256(256))) + + result = bls12_add(p1, p2) + + evm.output = g2_to_bytes(result) + + +def bls12_g2_msm(evm: Evm) -> None: + """ + The bls12_381 G2 multi-scalar multiplication precompile. + Note: This uses the naive approach to multi-scalar multiplication + which is not suitably optimized for production clients. Clients are + required to implement a more efficient algorithm such as the Pippenger + algorithm. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) == 0 or len(data) % LENGTH_PER_PAIR != 0: + raise InvalidParameter("Invalid Input Length") + + # GAS + k = len(data) // LENGTH_PER_PAIR + if k <= 128: + discount = Uint(G2_K_DISCOUNT[k - 1]) + else: + discount = Uint(G2_MAX_DISCOUNT) + + gas_cost = Uint(k) * GasCosts.PRECOMPILE_BLS_G2MUL * discount // MULTIPLIER + charge_gas(evm, gas_cost) + + # OPERATION + for i in range(k): + start_index = i * LENGTH_PER_PAIR + end_index = start_index + LENGTH_PER_PAIR + + p, m = decode_g2_scalar_pair(data[start_index:end_index]) + product = bls12_multiply(p, m) + + if i == 0: + result = product + else: + result = bls12_add(result, product) + + evm.output = g2_to_bytes(result) + + +def bls12_map_fp2_to_g2(evm: Evm) -> None: + """ + Precompile to map field element to G2. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid. + + """ + data = evm.message.data + if len(data) != 128: + raise InvalidParameter("Invalid Input Length") + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_BLS_G2MAP) + + # OPERATION + field_element = bytes_to_fq2(data) + assert isinstance(field_element, FQ2) + + fp2 = bytes_to_fq2(data) + g2_3d = clear_cofactor_G2(map_to_curve_G2(fp2)) + + evm.output = g2_to_bytes(g2_3d) diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py new file mode 100644 index 00000000000..c7a62cb49c0 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py @@ -0,0 +1,69 @@ +""" +Ethereum Virtual Machine (EVM) BLS12 381 PAIRING PRE-COMPILE. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the BLS12 381 pairing pre-compile. +""" + +from ethereum_types.numeric import Uint +from py_ecc.optimized_bls12_381 import FQ12, curve_order, is_inf, pairing +from py_ecc.optimized_bls12_381 import multiply as bls12_multiply + +from ....vm import Evm +from ....vm.gas import charge_gas +from ...exceptions import InvalidParameter +from . import bytes_to_g1, bytes_to_g2 + + +def bls12_pairing(evm: Evm) -> None: + """ + The bls12_381 pairing precompile. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + InvalidParameter + If the input length is invalid or if the subgroup check fails. + + """ + data = evm.message.data + if len(data) == 0 or len(data) % 384 != 0: + raise InvalidParameter("Invalid Input Length") + + # GAS + k = len(data) // 384 + gas_cost = Uint(32600 * k + 37700) + charge_gas(evm, gas_cost) + + # OPERATION + result = FQ12.one() + for i in range(k): + g1_start = Uint(384 * i) + g2_start = Uint(384 * i + 128) + + g1_slice = data[g1_start : g1_start + Uint(128)] + g1_point = bytes_to_g1(bytes(g1_slice)) + if not is_inf(bls12_multiply(g1_point, curve_order)): + raise InvalidParameter("Subgroup check failed for G1 point.") + + g2_slice = data[g2_start : g2_start + Uint(256)] + g2_point = bytes_to_g2(bytes(g2_slice)) + if not is_inf(bls12_multiply(g2_point, curve_order)): + raise InvalidParameter("Subgroup check failed for G2 point.") + + result *= pairing(g2_point, g1_point) + + if result == FQ12.one(): + evm.output = b"\x00" * 31 + b"\x01" + else: + evm.output = b"\x00" * 32 diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/ecrecover.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/ecrecover.py new file mode 100644 index 00000000000..17a0174f6ed --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/ecrecover.py @@ -0,0 +1,64 @@ +""" +Ethereum Virtual Machine (EVM) ECRECOVER PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `ECRECOVER` precompiled contract. +""" + +from ethereum_types.numeric import U256 + +from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import InvalidSignatureError +from ethereum.utils.byte import left_pad_zero_bytes + +from ...vm import Evm +from ...vm.gas import GasCosts, charge_gas +from ...vm.memory import buffer_read + + +def ecrecover(evm: Evm) -> None: + """ + Decrypts the address using elliptic curve DSA recovery mechanism and writes + the address to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_ECRECOVER) + + # OPERATION + message_hash_bytes = buffer_read(data, U256(0), U256(32)) + message_hash = Hash32(message_hash_bytes) + v = U256.from_be_bytes(buffer_read(data, U256(32), U256(32))) + r = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) + s = U256.from_be_bytes(buffer_read(data, U256(96), U256(32))) + + if v != U256(27) and v != U256(28): + return + if U256(0) >= r or r >= SECP256K1N: + return + if U256(0) >= s or s >= SECP256K1N: + return + + try: + public_key = secp256k1_recover(r, s, v - U256(27), message_hash) + except InvalidSignatureError: + # unable to extract public key + return + + address = keccak256(public_key)[12:32] + padded_address = left_pad_zero_bytes(address, 32) + evm.output = padded_address diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/identity.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/identity.py new file mode 100644 index 00000000000..b7631736074 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/identity.py @@ -0,0 +1,46 @@ +""" +Ethereum Virtual Machine (EVM) IDENTITY PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `IDENTITY` precompiled contract. +""" + +from ethereum_types.numeric import Uint, ulen + +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import ( + GasCosts, + charge_gas, +) + + +def identity(evm: Evm) -> None: + """ + Writes the message data to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + word_count = ceil32(ulen(data)) // Uint(32) + charge_gas( + evm, + GasCosts.PRECOMPILE_IDENTITY_BASE + + GasCosts.PRECOMPILE_IDENTITY_PER_WORD * word_count, + ) + + # OPERATION + evm.output = data diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/mapping.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/mapping.py new file mode 100644 index 00000000000..1d32bdce81d --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/mapping.py @@ -0,0 +1,78 @@ +""" +Precompiled Contract Addresses. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Mapping of precompiled contracts to their implementations. +""" + +from typing import Callable, Dict + +from ethereum.state import Address + +from . import ( + ALT_BN128_ADD_ADDRESS, + ALT_BN128_MUL_ADDRESS, + ALT_BN128_PAIRING_CHECK_ADDRESS, + BLAKE2F_ADDRESS, + BLS12_G1_ADD_ADDRESS, + BLS12_G1_MSM_ADDRESS, + BLS12_G2_ADD_ADDRESS, + BLS12_G2_MSM_ADDRESS, + BLS12_MAP_FP2_TO_G2_ADDRESS, + BLS12_MAP_FP_TO_G1_ADDRESS, + BLS12_PAIRING_ADDRESS, + ECRECOVER_ADDRESS, + IDENTITY_ADDRESS, + MODEXP_ADDRESS, + P256VERIFY_ADDRESS, + POINT_EVALUATION_ADDRESS, + RIPEMD160_ADDRESS, + SHA256_ADDRESS, +) +from .alt_bn128 import alt_bn128_add, alt_bn128_mul, alt_bn128_pairing_check +from .blake2f import blake2f +from .bls12_381.bls12_381_g1 import ( + bls12_g1_add, + bls12_g1_msm, + bls12_map_fp_to_g1, +) +from .bls12_381.bls12_381_g2 import ( + bls12_g2_add, + bls12_g2_msm, + bls12_map_fp2_to_g2, +) +from .bls12_381.bls12_381_pairing import bls12_pairing +from .ecrecover import ecrecover +from .identity import identity +from .modexp import modexp +from .p256verify import p256verify +from .point_evaluation import point_evaluation +from .ripemd160 import ripemd160 +from .sha256 import sha256 + +PRE_COMPILED_CONTRACTS: Dict[Address, Callable] = { + ECRECOVER_ADDRESS: ecrecover, + SHA256_ADDRESS: sha256, + RIPEMD160_ADDRESS: ripemd160, + IDENTITY_ADDRESS: identity, + MODEXP_ADDRESS: modexp, + ALT_BN128_ADD_ADDRESS: alt_bn128_add, + ALT_BN128_MUL_ADDRESS: alt_bn128_mul, + ALT_BN128_PAIRING_CHECK_ADDRESS: alt_bn128_pairing_check, + BLAKE2F_ADDRESS: blake2f, + POINT_EVALUATION_ADDRESS: point_evaluation, + BLS12_G1_ADD_ADDRESS: bls12_g1_add, + BLS12_G1_MSM_ADDRESS: bls12_g1_msm, + BLS12_G2_ADD_ADDRESS: bls12_g2_add, + BLS12_G2_MSM_ADDRESS: bls12_g2_msm, + BLS12_PAIRING_ADDRESS: bls12_pairing, + BLS12_MAP_FP_TO_G1_ADDRESS: bls12_map_fp_to_g1, + BLS12_MAP_FP2_TO_G2_ADDRESS: bls12_map_fp2_to_g2, + P256VERIFY_ADDRESS: p256verify, +} diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/modexp.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/modexp.py new file mode 100644 index 00000000000..bf828ee8f6e --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/modexp.py @@ -0,0 +1,175 @@ +""" +Ethereum Virtual Machine (EVM) MODEXP PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `MODEXP` precompiled contract. +""" + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U256, Uint + +from ...vm import Evm +from ...vm.exceptions import ExceptionalHalt +from ...vm.gas import charge_gas +from ..memory import buffer_read + + +def modexp(evm: Evm) -> None: + """ + Calculates `(base**exp) % modulus` for arbitrary sized `base`, `exp` and + `modulus`. The return value is the same length as the modulus. + """ + data = evm.message.data + + # GAS + base_length = U256.from_be_bytes(buffer_read(data, U256(0), U256(32))) + if base_length > U256(1024): + raise ExceptionalHalt("Mod-exp base length is too large") + + exp_length = U256.from_be_bytes(buffer_read(data, U256(32), U256(32))) + if exp_length > U256(1024): + raise ExceptionalHalt("Mod-exp exponent length is too large") + + modulus_length = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) + if modulus_length > U256(1024): + raise ExceptionalHalt("Mod-exp modulus length is too large") + + exp_start = U256(96) + base_length + + exp_head = U256.from_be_bytes( + buffer_read(data, exp_start, min(U256(32), exp_length)) + ) + + charge_gas( + evm, + gas_cost(base_length, modulus_length, exp_length, exp_head), + ) + + # OPERATION + if base_length == 0 and modulus_length == 0: + evm.output = Bytes() + return + + base = Uint.from_be_bytes(buffer_read(data, U256(96), base_length)) + exp = Uint.from_be_bytes(buffer_read(data, exp_start, exp_length)) + + modulus_start = exp_start + exp_length + modulus = Uint.from_be_bytes( + buffer_read(data, modulus_start, modulus_length) + ) + + if modulus == 0: + evm.output = Bytes(b"\x00") * modulus_length + else: + evm.output = pow(base, exp, modulus).to_bytes( + Uint(modulus_length), "big" + ) + + +def complexity(base_length: U256, modulus_length: U256) -> Uint: + """ + Estimate the complexity of performing a modular exponentiation. + + Parameters + ---------- + base_length : + Length of the array representing the base integer. + + modulus_length : + Length of the array representing the modulus integer. + + Returns + ------- + complexity : `Uint` + Complexity of performing the operation. + + """ + max_length = max(Uint(base_length), Uint(modulus_length)) + words = (max_length + Uint(7)) // Uint(8) + complexity = Uint(16) + if max_length > Uint(32): + complexity = Uint(2) * words ** Uint(2) + return complexity + + +def iterations(exponent_length: U256, exponent_head: U256) -> Uint: + """ + Calculate the number of iterations required to perform a modular + exponentiation. + + Parameters + ---------- + exponent_length : + Length of the array representing the exponent integer. + + exponent_head : + First 32 bytes of the exponent (with leading zero padding if it is + shorter than 32 bytes), as a U256. + + Returns + ------- + iterations : `Uint` + Number of iterations. + + """ + if exponent_length <= U256(32) and exponent_head == U256(0): + count = Uint(0) + elif exponent_length <= U256(32): + bit_length = exponent_head.bit_length() + + if bit_length > Uint(0): + bit_length -= Uint(1) + + count = bit_length + else: + length_part = Uint(16) * (Uint(exponent_length) - Uint(32)) + bits_part = exponent_head.bit_length() + + if bits_part > Uint(0): + bits_part -= Uint(1) + + count = length_part + bits_part + + return max(count, Uint(1)) + + +def gas_cost( + base_length: U256, + modulus_length: U256, + exponent_length: U256, + exponent_head: U256, +) -> Uint: + """ + Calculate the gas cost of performing a modular exponentiation. + + Parameters + ---------- + base_length : + Length of the array representing the base integer. + + modulus_length : + Length of the array representing the modulus integer. + + exponent_length : + Length of the array representing the exponent integer. + + exponent_head : + First 32 bytes of the exponent (with leading zero padding if it is + shorter than 32 bytes), as a U256. + + Returns + ------- + gas_cost : `Uint` + Gas required for performing the operation. + + """ + multiplication_complexity = complexity(base_length, modulus_length) + iteration_count = iterations(exponent_length, exponent_head) + cost = multiplication_complexity * iteration_count + return max(Uint(500), cost) diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/p256verify.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/p256verify.py new file mode 100644 index 00000000000..29c2e91e0f0 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/p256verify.py @@ -0,0 +1,90 @@ +""" +Ethereum Virtual Machine (EVM) P256VERIFY PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `P256VERIFY` precompiled contract. +""" + +from ethereum_types.numeric import U256 + +from ethereum.crypto.elliptic_curve import ( + SECP256R1N, + SECP256R1P, + is_on_curve_secp256r1, + secp256r1_verify, +) +from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import InvalidSignatureError +from ethereum.utils.byte import left_pad_zero_bytes + +from ...vm import Evm +from ...vm.gas import GasCosts, charge_gas +from ...vm.memory import buffer_read + + +def p256verify(evm: Evm) -> None: + """ + Verifies a P-256 signature. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_P256VERIFY) + + if len(data) != 160: + return + + # OPERATION + message_hash_bytes = buffer_read(data, U256(0), U256(32)) + message_hash = Hash32(message_hash_bytes) + r = U256.from_be_bytes(buffer_read(data, U256(32), U256(32))) + s = U256.from_be_bytes(buffer_read(data, U256(64), U256(32))) + public_key_x = U256.from_be_bytes( + buffer_read(data, U256(96), U256(32)) + ) # qx + public_key_y = U256.from_be_bytes( + buffer_read(data, U256(128), U256(32)) + ) # qy + + # Signature component bounds: + # Both r and s MUST satisfy 0 < r < n and 0 < s < n + if r <= U256(0) or r >= SECP256R1N: + return + if s <= U256(0) or s >= SECP256R1N: + return + + # Public key bounds: + # Both qx and qy MUST satisfy 0 ≤ qx < p and 0 ≤ qy < p + # U256 is unsigned, so we don't need to check for < 0 + if public_key_x >= SECP256R1P: + return + if public_key_y >= SECP256R1P: + return + + # Point should not be at infinity (represented as (0, 0)) + if public_key_x == U256(0) and public_key_y == U256(0): + return + + # Point validity: The point (qx, qy) MUST satisfy the curve equation + # qy^2 ≡ qx^3 + a*qx + b (mod p) + if not is_on_curve_secp256r1(public_key_x, public_key_y): + return + + try: + secp256r1_verify(r, s, public_key_x, public_key_y, message_hash) + except InvalidSignatureError: + return + + evm.output = left_pad_zero_bytes(b"\x01", 32) diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/point_evaluation.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/point_evaluation.py new file mode 100644 index 00000000000..d2d105ba13b --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/point_evaluation.py @@ -0,0 +1,72 @@ +""" +Ethereum Virtual Machine (EVM) POINT EVALUATION PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `POINT EVALUATION` precompiled contract. +""" + +from ethereum_types.bytes import Bytes, Bytes32, Bytes48 +from ethereum_types.numeric import U256 + +from ethereum.crypto.kzg import ( + KZGCommitment, + kzg_commitment_to_versioned_hash, + verify_kzg_proof, +) + +from ...vm import Evm +from ...vm.exceptions import KZGProofError +from ...vm.gas import GasCosts, charge_gas + +FIELD_ELEMENTS_PER_BLOB = 4096 +BLS_MODULUS = 52435875175126190479447740508185965837690552500527637822603658699938581184513 # noqa: E501 +VERSIONED_HASH_VERSION_KZG = b"\x01" + + +def point_evaluation(evm: Evm) -> None: + """ + A pre-compile that verifies a KZG proof which claims that a blob + (represented by a commitment) evaluates to a given value at a given point. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + if len(data) != 192: + raise KZGProofError + + versioned_hash = data[:32] + z = Bytes32(data[32:64]) + y = Bytes32(data[64:96]) + commitment = KZGCommitment(data[96:144]) + proof = Bytes48(data[144:192]) + + # GAS + charge_gas(evm, GasCosts.PRECOMPILE_POINT_EVALUATION) + if kzg_commitment_to_versioned_hash(commitment) != versioned_hash: + raise KZGProofError + + # Verify KZG proof with z and y in big endian format + try: + kzg_proof_verification = verify_kzg_proof(commitment, z, y, proof) + except Exception as e: + raise KZGProofError from e + + if not kzg_proof_verification: + raise KZGProofError + + # Return FIELD_ELEMENTS_PER_BLOB and BLS_MODULUS as padded + # 32 byte big endian values + evm.output = Bytes( + U256(FIELD_ELEMENTS_PER_BLOB).to_be_bytes32() + + U256(BLS_MODULUS).to_be_bytes32() + ) diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/ripemd160.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/ripemd160.py new file mode 100644 index 00000000000..c82c9bd534d --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/ripemd160.py @@ -0,0 +1,51 @@ +""" +Ethereum Virtual Machine (EVM) RIPEMD160 PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `RIPEMD160` precompiled contract. +""" + +import hashlib + +from ethereum_types.numeric import Uint, ulen + +from ethereum.utils.byte import left_pad_zero_bytes +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import ( + GasCosts, + charge_gas, +) + + +def ripemd160(evm: Evm) -> None: + """ + Writes the ripemd160 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + word_count = ceil32(ulen(data)) // Uint(32) + charge_gas( + evm, + GasCosts.PRECOMPILE_RIPEMD160_BASE + + GasCosts.PRECOMPILE_RIPEMD160_PER_WORD * word_count, + ) + + # OPERATION + hash_bytes = hashlib.new("ripemd160", data).digest() + padded_hash = left_pad_zero_bytes(hash_bytes, 32) + evm.output = padded_hash diff --git a/src/ethereum/forks/bogota/vm/precompiled_contracts/sha256.py b/src/ethereum/forks/bogota/vm/precompiled_contracts/sha256.py new file mode 100644 index 00000000000..9d467d7e951 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/precompiled_contracts/sha256.py @@ -0,0 +1,48 @@ +""" +Ethereum Virtual Machine (EVM) SHA256 PRECOMPILED CONTRACT. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the `SHA256` precompiled contract. +""" + +import hashlib + +from ethereum_types.numeric import Uint, ulen + +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import ( + GasCosts, + charge_gas, +) + + +def sha256(evm: Evm) -> None: + """ + Writes the sha256 hash to output. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + data = evm.message.data + + # GAS + word_count = ceil32(ulen(data)) // Uint(32) + charge_gas( + evm, + GasCosts.PRECOMPILE_SHA256_BASE + + GasCosts.PRECOMPILE_SHA256_PER_WORD * word_count, + ) + + # OPERATION + evm.output = hashlib.sha256(data).digest() diff --git a/src/ethereum/forks/bogota/vm/runtime.py b/src/ethereum/forks/bogota/vm/runtime.py new file mode 100644 index 00000000000..60fd42b52c9 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/runtime.py @@ -0,0 +1,95 @@ +""" +Ethereum Virtual Machine (EVM) Runtime Operations. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Runtime related operations used while executing EVM code. +""" + +from typing import Set + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import Uint, ulen + +from .instructions import Ops + + +def get_valid_jump_destinations(code: Bytes) -> Set[Uint]: + """ + Analyze the EVM code to obtain the set of valid jump destinations. + + Valid jump destinations are defined as follows: + * The jump destination is less than the length of the code. + * The jump destination should have the `JUMPDEST` opcode (0x5B). + * The jump destination shouldn't be part of the data corresponding to + `PUSH-N` opcodes. + * The jump destination shouldn't be part of the immediate byte + corresponding to `DUPN`, `SWAPN`, or `EXCHANGE` opcodes (EIP-8024). + + Note - Jump destinations are 0-indexed. + + Parameters + ---------- + code : + The EVM code which is to be executed. + + Returns + ------- + valid_jump_destinations: `Set[Uint]` + The set of valid jump destinations in the code. + + """ + valid_jump_destinations = set() + pc = Uint(0) + + while pc < ulen(code): + try: + current_opcode = Ops(code[pc]) + except ValueError: + # Skip invalid opcodes, as they don't affect the jumpdest + # analysis. Nevertheless, such invalid opcodes would be caught + # and raised when the interpreter runs. + pc += Uint(1) + continue + + if current_opcode == Ops.JUMPDEST: + valid_jump_destinations.add(pc) + elif Ops.PUSH1.value <= current_opcode.value <= Ops.PUSH32.value: + # If PUSH-N opcodes are encountered, skip the current opcode along + # with the trailing data segment corresponding to the PUSH-N + # opcodes. + push_data_size = current_opcode.value - Ops.PUSH1.value + 1 + pc += Uint(push_data_size) + elif current_opcode in (Ops.DUPN, Ops.SWAPN): + # EIP-8024: DUPN/SWAPN invalid immediate range is + # 90 < x < 128, i.e. 0x5B (91) to 0x7F (127). + # Invalid immediates are not skipped so the byte + # remains at an instruction boundary. + if ( + pc + Uint(1) < ulen(code) + and 0x5B <= code[pc + Uint(1)] <= 0x7F + ): + pass + else: + pc += Uint(1) + elif current_opcode == Ops.EXCHANGE: + # EIP-8024: EXCHANGE invalid immediate range is + # 81 < x < 128, i.e. 0x52 (82) to 0x7F (127). + # Invalid immediates are not skipped so the byte + # remains at an instruction boundary. + if ( + pc + Uint(1) < ulen(code) + and 0x52 <= code[pc + Uint(1)] <= 0x7F + ): + pass + else: + pc += Uint(1) + + pc += Uint(1) + + return valid_jump_destinations diff --git a/src/ethereum/forks/bogota/vm/stack.py b/src/ethereum/forks/bogota/vm/stack.py new file mode 100644 index 00000000000..98ba815cb73 --- /dev/null +++ b/src/ethereum/forks/bogota/vm/stack.py @@ -0,0 +1,131 @@ +""" +Ethereum Virtual Machine (EVM) Stack. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the stack operators for the EVM. +""" + +from typing import List, Tuple + +from ethereum_types.numeric import U8, U256 + +from .exceptions import ( + InvalidParameter, + StackOverflowError, + StackUnderflowError, +) + + +def decode_single(x: U8) -> U8: + """ + Decode the immediate byte for DUPN/SWAPN to get the stack index. + + Return n with 17 <= n <= 235. + + Parameters + ---------- + x : int + The immediate byte value (0-90 or 128-255). + + Returns + ------- + int + The stack index n, where 17 <= n <= 235. + + Raises + ------ + InvalidParameter + If x is in the forbidden range (90 < x < 128 or x > 255). + + """ + if not (U8(0) <= x <= U8(90) or U8(128) <= x <= U8(255)): + raise InvalidParameter( + f"DUPN/SWAPN immediate byte {x} is out of range. " + "Valid range: 0 <= x <= 90 or 128 <= x <= 255" + ) + + return U8((int(x) + 145) % 256) + + +def decode_pair(x: U8) -> Tuple[U8, U8]: + """ + Decode the immediate byte for EXCHANGE to get two stack indices. + + Return (n, m) with 1 <= n <= 14 and n < m <= 30 - n. + + Parameters + ---------- + x : int + The immediate byte value (0-81 or 128-255). + + Returns + ------- + Tuple[int, int] + The two stack indices (n, m), where + 1 <= n <= 14 and n < m <= 30 - n. + + Raises + ------ + InvalidParameter + If x is in the forbidden range (81 < x < 128 or x > 255). + + """ + if not (U8(0) <= x <= U8(81) or U8(128) <= x <= U8(255)): + raise InvalidParameter( + f"EXCHANGE immediate byte {x} is in the forbidden " + "range 82 <= x <= 127\n" + "Valid range: 0 <= x <= 81 or 128 <= x <= 255" + ) + + k = U8(int(x) ^ 143) + q, r = divmod(k, U8(16)) + if q < r: + return q + U8(1), r + U8(1) + else: + return r + U8(1), U8(29) - q + + +def pop(stack: List[U256]) -> U256: + """ + Pops the top item off of `stack`. + + Parameters + ---------- + stack : + EVM stack. + + Returns + ------- + value : `U256` + The top element on the stack. + + """ + if len(stack) == 0: + raise StackUnderflowError + + return stack.pop() + + +def push(stack: List[U256], value: U256) -> None: + """ + Pushes `value` onto `stack`. + + Parameters + ---------- + stack : + EVM stack. + + value : + Item to be pushed onto `stack`. + + """ + if len(stack) == 1024: + raise StackOverflowError + + return stack.append(value) From d14af34a8e71fad20580bad8607bad72a6a104d2 Mon Sep 17 00:00:00 2001 From: lightclient Date: Mon, 6 Jul 2026 06:21:01 -0600 Subject: [PATCH 2/9] eip-8141: implement frame transaction in bogota fork Implements the frame transaction (type 0x06) per the current EIP-8141 draft: - FrameTransaction, Frame, and TransactionSignature types with static validation, canonical signature hash (elided empty-msg signatures), and protocol signature validation (secp256k1, P-256, arbitrary) - derived transaction gas limit: intrinsic cost, per-frame cost, EIP-7623 calldata cost of the encoded frame/signature lists, signature verification gas, and the sum of frame gas limits - frame execution loop: DEFAULT/VERIFY/SENDER modes, per-frame gas accounting, shared warm-access journal, transient storage discard between frames, atomic batch rollback and skipping - APPROVE (0xaa): journaled approvals that revert with the granting call; payment approval increments the sender nonce and collects the maximum cost from the payer - introspection opcodes TXPARAM (0xb0), FRAMEDATALOAD (0xb1), FRAMEDATACOPY (0xb2), FRAMEPARAM (0xb3), SIGPARAM (0xb4) - default code for codeless accounts and the expiry verifier frame constraints - frame transaction receipt ([cumulative_gas_used, payer, [status, gas_used, logs]]) with skipped-frame status 0x3 - EIP-3607 exemption for frame transaction senders --- src/ethereum/forks/bogota/blocks.py | 83 ++- src/ethereum/forks/bogota/exceptions.py | 35 +- src/ethereum/forks/bogota/fork.py | 658 +++++++++++++++++- src/ethereum/forks/bogota/requests.py | 13 +- src/ethereum/forks/bogota/transactions.py | 609 +++++++++++++++- src/ethereum/forks/bogota/utils/message.py | 4 +- src/ethereum/forks/bogota/vm/__init__.py | 239 ++++++- src/ethereum/forks/bogota/vm/gas.py | 11 +- .../forks/bogota/vm/instructions/__init__.py | 14 + .../bogota/vm/instructions/environment.py | 296 +++++++- .../forks/bogota/vm/instructions/system.py | 73 +- src/ethereum/forks/bogota/vm/interpreter.py | 7 + vulture_whitelist.py | 8 + 13 files changed, 2021 insertions(+), 29 deletions(-) diff --git a/src/ethereum/forks/bogota/blocks.py b/src/ethereum/forks/bogota/blocks.py index 558ca2da7ae..6a80818d58d 100644 --- a/src/ethereum/forks/bogota/blocks.py +++ b/src/ethereum/forks/bogota/blocks.py @@ -25,6 +25,7 @@ AccessListTransaction, BlobTransaction, FeeMarketTransaction, + FrameTransaction, LegacyTransaction, SetCodeTransaction, Transaction, @@ -390,7 +391,68 @@ class Receipt: """ -def encode_receipt(tx: Transaction, receipt: Receipt) -> Bytes | Receipt: +@final +@slotted_freezable +@dataclass +class FrameReceipt: + """ + Result of executing a single frame of a frame transaction, as + defined in [EIP-8141]. + + [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 + """ + + status: Uint + """ + Return code of the top-level call of the frame: 0 for failure, 1 + for success, and 3 for a frame skipped due to a failed atomic + batch. + """ + + gas_used: Uint + """ + Gas consumed by the frame. + """ + + logs: Tuple[Log, ...] + """ + A tuple of logs generated by the frame. + """ + + +@final +@slotted_freezable +@dataclass +class FrameTransactionReceipt: + """ + Result of executing a frame transaction, as defined in [EIP-8141]. + Contains one [`FrameReceipt`] per frame instead of a single + transaction-level status. + + [`FrameReceipt`]: ref:ethereum.forks.bogota.blocks.FrameReceipt + [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 + """ + + cumulative_gas_used: Uint + """ + Total gas used in the block up to and including this transaction. + This is the gas used after refunds, paid by the payer. + """ + + payer: Address + """ + The address of the account that paid the fees for the transaction. + """ + + frame_receipts: Tuple[FrameReceipt, ...] + """ + The per-frame execution results, in frame order. + """ + + +def encode_receipt( + tx: Transaction, receipt: Receipt | FrameTransactionReceipt +) -> Bytes | Receipt: r""" Encodes a transaction receipt based on the transaction type. @@ -399,8 +461,17 @@ def encode_receipt(tx: Transaction, receipt: Receipt) -> Bytes | Receipt: - FeeMarketTransaction receipts are prefixed with `b"\x02"`. - BlobTransaction receipts are prefixed with `b"\x03"`. - SetCodeTransaction receipts are prefixed with `b"\x04"`. + - FrameTransaction receipts are prefixed with `b"\x06"` and use the + frame receipt payload defined in [EIP-8141]. - LegacyTransaction receipts are returned as is. + + [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 """ + if isinstance(tx, FrameTransaction): + assert isinstance(receipt, FrameTransactionReceipt) + return b"\x06" + rlp.encode(receipt) + + assert isinstance(receipt, Receipt) if isinstance(tx, AccessListTransaction): return b"\x01" + rlp.encode(receipt) elif isinstance(tx, FeeMarketTransaction): @@ -413,7 +484,9 @@ def encode_receipt(tx: Transaction, receipt: Receipt) -> Bytes | Receipt: return receipt -def decode_receipt(receipt: Bytes | Receipt) -> Receipt: +def decode_receipt( + receipt: Bytes | Receipt, +) -> Receipt | FrameTransactionReceipt: r""" Decodes a receipt from its serialized form. @@ -426,10 +499,14 @@ def decode_receipt(receipt: Bytes | Receipt) -> Receipt: receipts. - Receipts prefixed with `b"\x04"` are decoded as SetCodeTransaction receipts. + - Receipts prefixed with `b"\x06"` are decoded as FrameTransaction + receipts. - LegacyTransaction receipts are returned as is. """ if isinstance(receipt, Bytes): - assert receipt[0] in (1, 2, 3, 4) + assert receipt[0] in (1, 2, 3, 4, 6) + if receipt[0] == 6: + return rlp.decode_to(FrameTransactionReceipt, receipt[1:]) return rlp.decode_to(Receipt, receipt[1:]) else: return receipt diff --git a/src/ethereum/forks/bogota/exceptions.py b/src/ethereum/forks/bogota/exceptions.py index 6ef5651cfa8..43672c8a04c 100644 --- a/src/ethereum/forks/bogota/exceptions.py +++ b/src/ethereum/forks/bogota/exceptions.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Final -from ethereum_types.numeric import U64, Uint +from ethereum_types.numeric import U64, U256, Uint from ethereum.exceptions import InvalidBlock, InvalidTransaction @@ -20,7 +20,7 @@ class WrongChainIdError(InvalidTransaction): [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 """ - def __init__(self, expected: U64, actual: U64): + def __init__(self, expected: U64, actual: U64 | U256): super().__init__(f"expected chain_id `{expected}` but got `{actual}`") self.expected = expected self.actual = actual @@ -153,3 +153,34 @@ class BlockAccessListGasLimitExceededError(InvalidBlock): [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 """ + + +class FrameTransactionFormatError(InvalidTransaction): + """ + A frame transaction violates one of the static constraints defined + in [EIP-8141], such as an invalid frame count, mode, flags, or + signature entry structure. + + [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 + """ + + +class FrameTransactionSignatureError(InvalidTransaction): + """ + A signature entry of a frame transaction failed validation, as + defined in [EIP-8141]. + + [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 + """ + + +class FrameTransactionExecutionError(InvalidTransaction): + """ + Frame execution rendered the whole frame transaction invalid: a + `SENDER` frame ran before execution approval, a `VERIFY` frame + reverted, or no frame approved gas payment. + + See [EIP-8141] for the frame execution rules. + + [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 + """ diff --git a/src/ethereum/forks/bogota/fork.py b/src/ethereum/forks/bogota/fork.py index 85fca973be1..40ba1ba28be 100644 --- a/src/ethereum/forks/bogota/fork.py +++ b/src/ethereum/forks/bogota/fork.py @@ -12,10 +12,10 @@ """ from dataclasses import dataclass -from typing import Final, List, Optional, Tuple, final +from typing import Final, List, Optional, Set, Tuple, final from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.frozen import slotted_freezable from ethereum_types.numeric import U64, U256, Uint, ulen @@ -45,17 +45,29 @@ hash_block_access_list, validate_block_access_list_gas_limit, ) -from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt +from .blocks import ( + Block, + FrameReceipt, + FrameTransactionReceipt, + Header, + Log, + Receipt, + Withdrawal, + encode_receipt, +) from .bloom import logs_bloom from .exceptions import ( BlobCountExceededError, BlobGasLimitExceededError, EmptyAuthorizationListError, + FrameTransactionExecutionError, + FrameTransactionSignatureError, InsufficientMaxFeePerBlobGasError, InsufficientMaxFeePerGasError, InvalidBlobVersionedHashError, NoBlobDataError, PriorityFeeGreaterThanMaxFeeError, + TransactionGasLimitExceededError, TransactionTypeContractCreationError, WrongChainIdError, ) @@ -73,33 +85,59 @@ BlockState, TransactionState, clear_account_preserving_balance, + copy_tx_state, create_ether, extract_block_diff, get_account, get_code, incorporate_tx_into_block, increment_nonce, + restore_tx_state, set_account_balance, ) from .transactions import ( + APPROVE_EXECUTION, + APPROVE_NONE, + APPROVE_SCOPE_MASK, + ATOMIC_BATCH_FLAG, + ENTRY_POINT, + FRAME_MODE_SENDER, + FRAME_MODE_VERIFY, + FRAME_STATUS_FAILURE, + FRAME_STATUS_SKIPPED, + FRAME_STATUS_SUCCESS, + SIGNATURE_SCHEME_SECP256K1, TX_MAX_GAS_LIMIT, BlobTransaction, FeeMarketCapableTransaction, + Frame, + FrameTransaction, LegacyTransaction, SetCodeTransaction, + StandardTransaction, Transaction, chain_id, + compute_frame_signature_hash, decode_transaction, encode_transaction, get_transaction_hash, has_access_list, recover_sender, + resolve_frame_target, + validate_frame_signature, + validate_frame_transaction, validate_transaction, ) from .utils.hexadecimal import hex_to_address from .utils.message import prepare_message -from .vm import Message +from .vm import ( + FrameTransactionContext, + Message, + apply_frame_approval, + attempt_frame_approval, +) from .vm.eoa_delegation import is_valid_delegation +from .vm.exceptions import Revert from .vm.gas import ( GasCosts, StateGasCosts, @@ -109,6 +147,7 @@ calculate_total_blob_gas, ) from .vm.interpreter import MessageCallOutput, process_message_call +from .vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS BASE_FEE_MAX_CHANGE_DENOMINATOR = Uint(8) ELASTICITY_MULTIPLIER = Uint(2) @@ -502,7 +541,7 @@ def validate_header( def check_transaction( block_env: vm.BlockEnvironment, block_output: vm.BlockOutput, - tx: Transaction, + tx: StandardTransaction, sender: Address, tx_state: TransactionState, ) -> Tuple[Uint, Tuple[VersionedHash, ...], U64]: @@ -1024,6 +1063,10 @@ def process_transaction( encode_transaction(tx), ) + if isinstance(tx, FrameTransaction): + process_frame_transaction(block_env, block_output, tx, index, tx_state) + return + tx_chain_id = chain_id(tx) if tx_chain_id is not None and tx_chain_id != block_env.chain_id: raise WrongChainIdError( @@ -1176,6 +1219,611 @@ def process_transaction( incorporate_tx_into_block(tx_state, block_env.block_access_list_builder) +def check_frame_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: FrameTransaction, + tx_state: TransactionState, + tx_gas_limit: Uint, +) -> Tuple[Uint, U64]: + """ + Check if the frame transaction is includable in the block. + + Frame transactions are checked like ordinary transactions except + that the gas limit is derived from the transaction contents, no + upfront balance check is performed (fees are collected from the + payer during frame execution), and the sender account may have + code, exempting it from [EIP-3607]. + + Parameters + ---------- + block_env : + The block scoped environment. + block_output : + The block output for the current block. + tx : + The frame transaction. + tx_state : + The transaction state tracker. + tx_gas_limit : + The derived total gas limit of the transaction. + + Returns + ------- + effective_gas_price : + The price to charge for gas when the transaction is executed. + tx_blob_gas_used : + The blob gas used by the transaction. + + Raises + ------ + InvalidTransaction : + If the transaction is not includable. + + [EIP-3607]: https://eips.ethereum.org/EIPS/eip-3607 + + """ + if tx.chain_id != U256(block_env.chain_id): + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx.chain_id, + ) + + regular_gas_available = ( + block_env.block_gas_limit - block_output.block_gas_used + ) + state_gas_available = ( + block_env.block_gas_limit - block_output.block_state_gas_used + ) + blob_gas_available = MAX_BLOB_GAS_PER_BLOCK - block_output.blob_gas_used + + if tx_gas_limit > TX_MAX_GAS_LIMIT: + raise TransactionGasLimitExceededError( + "transaction gas limit exceeds TX_MAX_GAS_LIMIT" + ) + if tx_gas_limit > regular_gas_available: + raise GasUsedExceedsLimitError("regular gas used exceeds limit") + if tx_gas_limit > state_gas_available: + raise GasUsedExceedsLimitError("state gas used exceeds limit") + + tx_blob_gas_used = calculate_total_blob_gas(tx) + if tx_blob_gas_used > blob_gas_available: + raise BlobGasLimitExceededError("blob gas limit exceeded") + + if tx.max_fee_per_gas < tx.max_priority_fee_per_gas: + raise PriorityFeeGreaterThanMaxFeeError( + "priority fee greater than max fee" + ) + if tx.max_fee_per_gas < block_env.base_fee_per_gas: + raise InsufficientMaxFeePerGasError( + tx.max_fee_per_gas, block_env.base_fee_per_gas + ) + + priority_fee_per_gas = min( + tx.max_priority_fee_per_gas, + tx.max_fee_per_gas - block_env.base_fee_per_gas, + ) + effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas + + blob_count = len(tx.blob_versioned_hashes) + if blob_count > 0: + if blob_count > BLOB_COUNT_LIMIT: + raise BlobCountExceededError( + f"Tx has {blob_count} blobs. Max allowed: {BLOB_COUNT_LIMIT}" + ) + for blob_versioned_hash in tx.blob_versioned_hashes: + if blob_versioned_hash[0:1] != VERSIONED_HASH_VERSION_KZG: + raise InvalidBlobVersionedHashError( + "invalid blob versioned hash" + ) + + blob_gas_price = calculate_blob_gas_price(block_env.excess_blob_gas) + if Uint(tx.max_fee_per_blob_gas) < blob_gas_price: + raise InsufficientMaxFeePerBlobGasError( + "insufficient max fee per blob gas" + ) + + sender_account = get_account(tx_state, tx.sender) + if sender_account.nonce > Uint(tx.nonce): + raise NonceMismatchError("nonce too low") + elif sender_account.nonce < Uint(tx.nonce): + raise NonceMismatchError("nonce too high") + + return effective_gas_price, tx_blob_gas_used + + +def execute_default_verify_frame( + frame_context: FrameTransactionContext, + frame: Frame, + resolved_target: Address, + tx_state: TransactionState, +) -> MessageCallOutput: + """ + Execute the default code behavior of a `VERIFY` frame whose + resolved target has no code. + + The default code approves the scope allowed by the frame's flags, + provided the transaction carries a secp256k1 signature entry from + the resolved target over the canonical signature hash. + + Parameters + ---------- + frame_context : + The context of the executing frame transaction. + frame : + The frame being executed. + resolved_target : + The resolved target of the frame. + tx_state : + The transaction state. + + Returns + ------- + frame_output : `MessageCallOutput` + The synthesized output of the default code execution. + + """ + tx = frame_context.tx + + def failure(reason: str) -> MessageCallOutput: + return default_code_output(frame, error=Revert(reason)) + + allowed_scope = frame.flags & APPROVE_SCOPE_MASK + if allowed_scope == APPROVE_NONE: + return failure("no approval scope allowed") + if allowed_scope & APPROVE_EXECUTION and resolved_target != tx.sender: + return failure("execution scope outside sender") + + has_sender_signature = any( + sig.scheme == SIGNATURE_SCHEME_SECP256K1 + and sig.signer == resolved_target + and len(sig.msg) == 0 + for sig in tx.signatures + ) + if not has_sender_signature: + return failure("no matching secp256k1 signature") + + approval = attempt_frame_approval( + frame_context=frame_context, + scope=allowed_scope, + frame_flags=frame.flags, + resolved_target=resolved_target, + sender_approved=frame_context.sender_approved, + payer=frame_context.payer, + tx_state=tx_state, + ) + if approval is None: + return failure("approval not granted") + + output = default_code_output(frame, error=None) + output.approvals = (approval,) + return output + + +def default_code_output( + frame: Frame, error: Optional[EthereumException] +) -> MessageCallOutput: + """ + Build the output of a frame executed via the default code, which + consumes no gas and produces no return data. + + Parameters + ---------- + frame : + The frame that was executed. + error : + The execution error, if the default code reverted. + + Returns + ------- + frame_output : `MessageCallOutput` + The synthesized output. + + """ + return MessageCallOutput( + gas_left=frame.gas_limit, + refund_counter=U256(0), + logs=(), + accounts_to_delete=set(), + error=error, + return_data=Bytes(b""), + state_gas_left=Uint(0), + regular_gas_used=Uint(0), + state_gas_used=0, + state_refund=Uint(0), + created_target_alive=False, + ) + + +def execute_frame( + block_env: vm.BlockEnvironment, + tx_env: vm.TransactionEnvironment, + frame_context: FrameTransactionContext, + frame: Frame, + frame_caller: Address, + resolved_target: Address, + accessed_addresses: Set[Address], + accessed_storage_keys: Set[Tuple[Address, Bytes32]], +) -> MessageCallOutput: + """ + Execute a single frame of a frame transaction. + + `VERIFY` frames execute with static-call semantics. Frames whose + resolved target has no code execute the default code: `VERIFY` + frames approve based on the transaction signatures while `DEFAULT` + and `SENDER` frames behave as calls to empty code. + + The shared warm-access journal is copied for the duration of the + frame and merged back only when the frame succeeds, so that a + failed frame does not leave accounts warm. + + Parameters + ---------- + block_env : + The block scoped environment. + tx_env : + The transaction scoped environment. + frame_context : + The context of the executing frame transaction. + frame : + The frame to execute. + frame_caller : + The caller of the frame: the entry point or the sender. + resolved_target : + The resolved target of the frame. + accessed_addresses : + Warm addresses shared across frames. + accessed_storage_keys : + Warm storage keys shared across frames. + + Returns + ------- + frame_output : `MessageCallOutput` + The output of the frame execution. + + """ + tx_state = tx_env.state + target_account = get_account(tx_state, resolved_target) + + if ( + target_account.code_hash == EMPTY_CODE_HASH + and frame.mode == FRAME_MODE_VERIFY + ): + return execute_default_verify_frame( + frame_context, frame, resolved_target, tx_state + ) + + # As with an ordinary `CALL`, a frame whose caller cannot cover the + # transferred value reverts without executing. + caller_balance = get_account(tx_state, frame_caller).balance + if U256(caller_balance) < frame.value: + return default_code_output( + frame, error=Revert("insufficient balance for frame value") + ) + + frame_accessed_addresses = set(accessed_addresses) + frame_accessed_addresses.add(resolved_target) + frame_accessed_addresses.add(frame_caller) + frame_accessed_storage_keys = set(accessed_storage_keys) + + message = Message( + block_env=block_env, + tx_env=tx_env, + caller=frame_caller, + target=resolved_target, + gas=frame.gas_limit, + state_gas_reservoir=Uint(0), + value=frame.value, + data=frame.data, + code=get_code(tx_state, target_account.code_hash), + depth=Uint(0), + current_target=resolved_target, + code_address=resolved_target, + should_transfer_value=frame.value != 0, + is_static=frame.mode == FRAME_MODE_VERIFY, + accessed_addresses=frame_accessed_addresses, + accessed_storage_keys=frame_accessed_storage_keys, + disable_precompiles=False, + parent_evm=None, + ) + + frame_output = process_message_call(message) + + if frame_output.error is None: + accessed_addresses.update(frame_accessed_addresses) + accessed_storage_keys.update(frame_accessed_storage_keys) + + return frame_output + + +def process_frame_transaction( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + tx: FrameTransaction, + index: Uint, + tx_state: TransactionState, +) -> None: + """ + Execute a frame transaction against the provided environment. + + After the static constraints and signature entries are validated, + each frame executes in order as a top-level call. Approvals granted + by the `APPROVE` instruction accumulate in the transaction-scoped + context: execution approval unlocks `SENDER` frames and payment + approval collects the maximum transaction cost from the payer. + After all frames have run a payer must have been set, unused gas is + refunded to the payer, and the priority fee is paid to the + coinbase. + + Parameters + ---------- + block_env : + Environment for the Ethereum Virtual Machine. + block_output : + The block output for the current block. + tx : + The frame transaction to execute. + index : + Index of the transaction in the block. + tx_state : + The transaction state tracker. + + """ + tx_gas_limit = validate_frame_transaction(tx) + + effective_gas_price, tx_blob_gas_used = check_frame_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + tx_state=tx_state, + tx_gas_limit=tx_gas_limit, + ) + + sig_hash = compute_frame_signature_hash(tx) + for sig in tx.signatures: + if not validate_frame_signature(sig, sig_hash): + raise FrameTransactionSignatureError("invalid signature entry") + + blob_gas_fee = calculate_data_fee(block_env.excess_blob_gas, tx) + max_cost = tx_gas_limit * tx.max_fee_per_gas + Uint( + tx_blob_gas_used + ) * Uint(tx.max_fee_per_blob_gas) + + frame_context = FrameTransactionContext( + tx=tx, + sig_hash=sig_hash, + max_cost=max_cost, + ) + + total_frame_gas_limit = Uint(0) + for frame in tx.frames: + total_frame_gas_limit += frame.gas_limit + intrinsic_gas = tx_gas_limit - total_frame_gas_limit + + tx_env = vm.TransactionEnvironment( + origin=ENTRY_POINT, + recipient=tx.sender, + value=U256(0), + gas_price=effective_gas_price, + gas=Uint(0), + state_gas_reservoir=Uint(0), + access_list_addresses=set(), + access_list_storage_keys=set(), + state=tx_state, + blob_versioned_hashes=tx.blob_versioned_hashes, + authorizations=(), + index_in_block=index, + tx_hash=get_transaction_hash(encode_transaction(tx)), + intrinsic_regular_gas=intrinsic_gas, + intrinsic_state_gas=Uint(0), + frame_context=frame_context, + ) + + # The warm-access journal is shared across frames. + accessed_addresses: Set[Address] = set() + accessed_addresses.add(tx.sender) + accessed_addresses.add(ENTRY_POINT) + accessed_addresses.add(block_env.coinbase) + accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) + accessed_storage_keys: Set[Tuple[Address, Bytes32]] = set() + + frame_receipts: List[FrameReceipt] = [] + frame_refunds: List[U256] = [] + frame_gas_used: List[Uint] = [] + frame_state_gas_used: List[int] = [] + frame_accounts_to_delete: List[Set[Address]] = [] + + in_batch = False + skip_batch = False + batch_start_index = 0 + batch_snapshot: Optional[TransactionState] = None + batch_approval_snapshot: Tuple[bool, Optional[Address]] = (False, None) + + for i, frame in enumerate(tx.frames): + frame_context.current_frame_index = Uint(i) + resolved_target = resolve_frame_target(tx, frame) + has_batch_flag = bool(frame.flags & ATOMIC_BATCH_FLAG) + + # A frame with the atomic batch flag opens a batch that runs up + # to and including the next frame without the flag. + if has_batch_flag and not in_batch: + in_batch = True + batch_start_index = i + batch_snapshot = copy_tx_state(tx_state) + batch_approval_snapshot = ( + frame_context.sender_approved, + frame_context.payer, + ) + terminates_batch = in_batch and not has_batch_flag + + if skip_batch: + # The gas of skipped frames is never charged and therefore + # implicitly refunded to the payer. + frame_context.frame_statuses.append(FRAME_STATUS_SKIPPED) + frame_receipts.append( + FrameReceipt( + status=FRAME_STATUS_SKIPPED, + gas_used=Uint(0), + logs=(), + ) + ) + frame_refunds.append(U256(0)) + frame_gas_used.append(Uint(0)) + frame_state_gas_used.append(0) + frame_accounts_to_delete.append(set()) + if terminates_batch: + in_batch = False + skip_batch = False + batch_snapshot = None + continue + + if frame.mode == FRAME_MODE_SENDER: + if not frame_context.sender_approved: + raise FrameTransactionExecutionError( + "SENDER frame before execution approval" + ) + frame_caller = tx.sender + else: + frame_caller = ENTRY_POINT + + # Transient storage is discarded between frames. + tx_state.transient_storage.clear() + + tx_env.origin = frame_caller + tx_env.gas = frame.gas_limit + tx_env.recipient = resolved_target + tx_env.value = frame.value + + frame_output = execute_frame( + block_env=block_env, + tx_env=tx_env, + frame_context=frame_context, + frame=frame, + frame_caller=frame_caller, + resolved_target=resolved_target, + accessed_addresses=accessed_addresses, + accessed_storage_keys=accessed_storage_keys, + ) + + for approval in frame_output.approvals: + apply_frame_approval(frame_context, approval) + + if frame_output.error is not None: + if frame.mode == FRAME_MODE_VERIFY: + raise FrameTransactionExecutionError("VERIFY frame reverted") + status = FRAME_STATUS_FAILURE + logs: Tuple[Log, ...] = () + else: + status = FRAME_STATUS_SUCCESS + logs = frame_output.logs + + frame_context.frame_statuses.append(status) + frame_receipts.append( + FrameReceipt( + status=status, + gas_used=frame.gas_limit - frame_output.gas_left, + logs=logs, + ) + ) + frame_refunds.append(frame_output.refund_counter) + frame_gas_used.append(frame.gas_limit - frame_output.gas_left) + frame_state_gas_used.append(max(0, frame_output.state_gas_used)) + frame_accounts_to_delete.append(set(frame_output.accounts_to_delete)) + + if status == FRAME_STATUS_FAILURE and in_batch: + # Unroll the atomic batch: restore the state to the + # condition immediately before the batch began and discard + # the effects of the already executed batch frames. Their + # gas remains charged. + assert batch_snapshot is not None + restore_tx_state(tx_state, batch_snapshot) + ( + frame_context.sender_approved, + frame_context.payer, + ) = batch_approval_snapshot + for j in range(batch_start_index, i): + frame_context.frame_statuses[j] = FRAME_STATUS_FAILURE + frame_receipts[j] = FrameReceipt( + status=FRAME_STATUS_FAILURE, + gas_used=frame_receipts[j].gas_used, + logs=(), + ) + frame_refunds[j] = U256(0) + frame_state_gas_used[j] = 0 + frame_accounts_to_delete[j] = set() + if terminates_batch: + in_batch = False + batch_snapshot = None + else: + skip_batch = True + elif terminates_batch: + in_batch = False + batch_snapshot = None + + if frame_context.payer is None: + raise FrameTransactionExecutionError("no frame approved gas payment") + payer = frame_context.payer + + total_frames_gas_used = Uint(0) + for gas_used in frame_gas_used: + total_frames_gas_used += gas_used + + tx_gas_used_before_refund = intrinsic_gas + total_frames_gas_used + refund_counter = U256(0) + for refund in frame_refunds: + refund_counter += refund + tx_gas_refund = min( + tx_gas_used_before_refund // Uint(5), Uint(refund_counter) + ) + tx_gas_used = tx_gas_used_before_refund - tx_gas_refund + + # Refund the payer everything beyond the actual transaction fee. + actual_fee = tx_gas_used * effective_gas_price + blob_gas_fee + create_ether(tx_state, payer, U256(max_cost - actual_fee)) + + # transfer miner fees + priority_fee_per_gas = effective_gas_price - block_env.base_fee_per_gas + create_ether( + tx_state, block_env.coinbase, U256(tx_gas_used * priority_fee_per_gas) + ) + + tx_state_gas = 0 + for state_gas in frame_state_gas_used: + tx_state_gas += state_gas + tx_regular_gas = tx_gas_used_before_refund - Uint(tx_state_gas) + block_output.block_gas_used += tx_regular_gas + block_output.block_state_gas_used += Uint(tx_state_gas) + block_output.blob_gas_used += tx_blob_gas_used + + block_output.cumulative_gas_used += tx_gas_used + receipt = encode_receipt( + tx, + FrameTransactionReceipt( + cumulative_gas_used=block_output.cumulative_gas_used, + payer=payer, + frame_receipts=tuple(frame_receipts), + ), + ) + + receipt_key = rlp.encode(Uint(index)) + block_output.receipt_keys += (receipt_key,) + + trie_set( + block_output.receipts_trie, + receipt_key, + receipt, + ) + + for frame_receipt in frame_receipts: + block_output.block_logs += frame_receipt.logs + + for accounts_to_delete in frame_accounts_to_delete: + for address in accounts_to_delete: + clear_account_preserving_balance(tx_state, address) + + incorporate_tx_into_block(tx_state, block_env.block_access_list_builder) + + def process_withdrawals( block_env: vm.BlockEnvironment, block_output: vm.BlockOutput, diff --git a/src/ethereum/forks/bogota/requests.py b/src/ethereum/forks/bogota/requests.py index f755d63427b..71b0a29ce6c 100644 --- a/src/ethereum/forks/bogota/requests.py +++ b/src/ethereum/forks/bogota/requests.py @@ -33,7 +33,7 @@ """ from hashlib import sha256 -from typing import List +from typing import List, Tuple from ethereum_types.bytes import Bytes from ethereum_types.numeric import Uint, ulen @@ -42,7 +42,7 @@ from ethereum.merkle_patricia_trie import trie_get from ethereum.utils.hexadecimal import hex_to_bytes32 -from .blocks import decode_receipt +from .blocks import FrameTransactionReceipt, Log, decode_receipt from .utils.hexadecimal import hex_to_address from .vm import BlockOutput @@ -292,7 +292,14 @@ def parse_deposit_requests(block_output: BlockOutput) -> Bytes: receipt = trie_get(block_output.receipts_trie, key) assert receipt is not None decoded_receipt = decode_receipt(receipt) - for log in decoded_receipt.logs: + logs: Tuple[Log, ...] + if isinstance(decoded_receipt, FrameTransactionReceipt): + logs = () + for frame_receipt in decoded_receipt.frame_receipts: + logs += frame_receipt.logs + else: + logs = decoded_receipt.logs + for log in logs: if log.address == DEPOSIT_CONTRACT_ADDRESS: if ( len(log.topics) > 0 diff --git a/src/ethereum/forks/bogota/transactions.py b/src/ethereum/forks/bogota/transactions.py index 9a0c76a007c..28776b950b4 100644 --- a/src/ethereum/forks/bogota/transactions.py +++ b/src/ethereum/forks/bogota/transactions.py @@ -4,7 +4,7 @@ transactions are the events that move between states. """ -from dataclasses import dataclass +from dataclasses import dataclass, replace from typing import Tuple, TypeGuard, final from ethereum_rlp import rlp @@ -12,7 +12,11 @@ from ethereum_types.frozen import slotted_freezable from ethereum_types.numeric import U64, U256, Uint, ulen -from ethereum.crypto.elliptic_curve import SECP256K1N, secp256k1_recover +from ethereum.crypto.elliptic_curve import ( + SECP256K1N, + secp256k1_recover, + secp256r1_verify, +) from ethereum.crypto.hash import Hash32, keccak256 from ethereum.exceptions import ( InsufficientTransactionGasError, @@ -22,6 +26,7 @@ from ethereum.state import Address from .exceptions import ( + FrameTransactionFormatError, InitCodeTooLargeError, TransactionTypeError, ) @@ -75,6 +80,179 @@ class IntrinsicGasCost: [EIP-7981]: https://eips.ethereum.org/EIPS/eip-7981 """ +FRAME_TX_INTRINSIC_COST = Uint(15000) +""" +Base intrinsic cost of a frame transaction per [EIP-8141]. + +[EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 +""" + +FRAME_TX_PER_FRAME_COST = Uint(475) +""" +Fixed cost charged for each frame in a frame transaction per [EIP-8141]. +It covers the call-context overhead of the frame boundary and the +per-frame receipt entry. + +[EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 +""" + +ENTRY_POINT = Address(b"\x00" * 19 + b"\xaa") +""" +Address used as the caller of `DEFAULT` and `VERIFY` mode frames per +[EIP-8141]. + +[EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 +""" + +EXPIRY_VERIFIER = Address(b"\x00" * 18 + b"\x81\x41") +""" +Address of the expiry verifier contract per [EIP-8141]. A `VERIFY` frame +targeting this address checks that the transaction has not expired. + +[EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 +""" + +EXPIRY_VERIFIER_CODE = Bytes( + bytes.fromhex("60083614600a575f5ffd5b5f3560c01c4211601657005b5f5ffd") +) +""" +Runtime code installed at [`EXPIRY_VERIFIER`] per [EIP-8141]. It reverts +unless the calldata is an 8-byte big-endian timestamp that is greater +than or equal to the block timestamp. + +[`EXPIRY_VERIFIER`]: ref:ethereum.forks.bogota.transactions.EXPIRY_VERIFIER +[EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 +""" + +EXPIRY_DATA_LENGTH = 8 +""" +Required length of the calldata of an expiry verifier frame per +[EIP-8141]. + +[EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 +""" + +MAX_FRAMES = 64 +""" +Maximum number of frames in a frame transaction per [EIP-8141]. + +[EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 +""" + +FRAME_MODE_DEFAULT = Uint(0) +""" +Frame mode executing the frame as a call from [`ENTRY_POINT`]. + +[`ENTRY_POINT`]: ref:ethereum.forks.bogota.transactions.ENTRY_POINT +""" + +FRAME_MODE_VERIFY = Uint(1) +""" +Frame mode identifying the frame as transaction validation. The frame +executes with static-call semantics; only `APPROVE` may modify state. +A reverting `VERIFY` frame invalidates the whole transaction. +""" + +FRAME_MODE_SENDER = Uint(2) +""" +Frame mode executing the frame as a call from the transaction sender. +Requires prior execution approval. +""" + +FRAME_MODE_COUNT = Uint(3) +""" +Number of defined frame modes. `frame.mode` must be strictly less than +this value. +""" + +ATOMIC_BATCH_FLAG = Uint(0x4) +""" +Bit 2 of `frame.flags`: the frame forms an atomic batch with the frames +that follow it, up to and including the first frame without this flag. +""" + +FRAME_FLAGS_LIMIT = Uint(8) +""" +Exclusive upper bound of the `frame.flags` field. Higher bits are +reserved. +""" + +APPROVE_NONE = Uint(0x0) +""" +Approval scope permitting no approval at all. +""" + +APPROVE_PAYMENT = Uint(0x1) +""" +Approval scope where the approving contract pays the total gas cost of +the transaction. +""" + +APPROVE_EXECUTION = Uint(0x2) +""" +Approval scope where the sender contract approves future frames calling +on its behalf. Only valid when the frame's resolved target equals the +transaction sender. +""" + +APPROVE_EXECUTION_AND_PAYMENT = Uint(0x3) +""" +Approval scope combining [`APPROVE_PAYMENT`] and [`APPROVE_EXECUTION`]. + +[`APPROVE_PAYMENT`]: ref:ethereum.forks.bogota.transactions.APPROVE_PAYMENT +[`APPROVE_EXECUTION`]: ref:ethereum.forks.bogota.transactions.APPROVE_EXECUTION +""" + +APPROVE_SCOPE_MASK = APPROVE_EXECUTION_AND_PAYMENT +""" +Mask extracting the approval scope from `frame.flags` (bits 0-1). +""" + +SIGNATURE_SCHEME_ARBITRARY = Uint(0x0) +""" +Signature scheme carrying arbitrary witness bytes that are not validated +by the protocol. +""" + +SIGNATURE_SCHEME_SECP256K1 = Uint(0x1) +""" +Signature scheme for secp256k1 signatures encoded as +`v (1 byte) || r (32 bytes) || s (32 bytes)`. +""" + +SIGNATURE_SCHEME_P256 = Uint(0x2) +""" +Signature scheme for P-256 signatures encoded as +`r || s || qx || qy` (each 32 bytes). +""" + +SECP256K1_SIGNATURE_VERIFICATION_GAS = Uint(2800) +""" +Gas charged for protocol validation of a secp256k1 signature entry. +""" + +P256_SIGNATURE_VERIFICATION_GAS = Uint(6700) +""" +Gas charged for protocol validation of a P-256 signature entry. +""" + +FRAME_STATUS_FAILURE = Uint(0) +""" +Frame receipt status of a frame whose execution reverted or was rolled +back as part of a failed atomic batch. +""" + +FRAME_STATUS_SUCCESS = Uint(1) +""" +Frame receipt status of a frame that executed successfully. +""" + +FRAME_STATUS_SKIPPED = Uint(3) +""" +Frame receipt status of a frame that was skipped because an earlier +frame of its atomic batch failed. +""" + @final @slotted_freezable @@ -483,7 +661,159 @@ class SetCodeTransaction: """ -Transaction = ( +@final +@slotted_freezable +@dataclass +class Frame: + """ + A single execution frame of a [`FrameTransaction`][ft], as defined + in [EIP-8141]. A frame is a contract call that validates the + transaction, approves gas payment, or executes a user operation. + + [ft]: ref:ethereum.forks.bogota.transactions.FrameTransaction + [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 + """ + + mode: Uint + """ + The execution semantics of the frame: 0 = `DEFAULT`, 1 = `VERIFY`, + 2 = `SENDER`. + """ + + flags: Uint + """ + Optional frame features. Bits 0-1 are the allowed approval scope and + bit 2 marks the frame as part of an atomic batch. + """ + + target: Bytes0 | Address + """ + The destination address of the frame. If empty, the frame targets + the transaction sender. + """ + + gas_limit: Uint + """ + The maximum gas allowed to be used by the frame. + """ + + value: U256 + """ + The amount in wei transferred from the sender as part of the frame + execution. Must be zero unless the frame mode is `SENDER`. + """ + + data: Bytes + """ + The calldata provided to the top level call of the frame. + """ + + +@final +@slotted_freezable +@dataclass +class TransactionSignature: + """ + A signature entry of a [`FrameTransaction`][ft], as defined in + [EIP-8141]. Signature entries are validated by the protocol before + any frame executes and may be referenced by `VERIFY` frames and by + ordinary EVM execution through the `SIGPARAM` instruction. + + [ft]: ref:ethereum.forks.bogota.transactions.FrameTransaction + [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 + """ + + scheme: Uint + """ + The verification scheme used to interpret the raw signature bytes: + 0 = `ARBITRARY`, 1 = `SECP256K1`, 2 = `P256`. + """ + + signer: Bytes + """ + Scheme-dependent signer metadata. A 20-byte address for `SECP256K1` + and `P256`; empty for `ARBITRARY`. + """ + + msg: Bytes + """ + Either empty, indicating the canonical transaction signature hash, + or an explicit 32-byte digest. The explicit 32-byte zero digest is + invalid. + """ + + signature: Bytes + """ + Raw signature bytes interpreted according to `scheme`. + """ + + +@final +@slotted_freezable +@dataclass +class FrameTransaction: + """ + The transaction type added in [EIP-8141]. + + A frame transaction decomposes into a sequence of frames — contract + calls that validate the transaction, approve gas payment, and + execute user operations — allowing validity and gas payment to be + defined abstractly by account code. + + [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 + """ + + chain_id: U256 + """ + The ID of the chain on which this transaction is executed. + """ + + nonce: U64 + """ + A scalar value equal to the number of transactions sent by the + sender. + """ + + sender: Address + """ + The address of the intended sender of the transaction. + """ + + frames: Tuple[Frame, ...] + """ + The ordered list of frames to execute. + """ + + signatures: Tuple[TransactionSignature, ...] + """ + The list of validated signatures available to the transaction. + """ + + max_priority_fee_per_gas: Uint + """ + The maximum priority fee per gas that the sender is willing to pay. + """ + + max_fee_per_gas: Uint + """ + The maximum fee per gas that the sender is willing to pay, including + the base fee and priority fee. + """ + + max_fee_per_blob_gas: U256 + """ + The maximum fee per blob gas that the sender is willing to pay. Must + be zero if `blob_versioned_hashes` is empty. + """ + + blob_versioned_hashes: Tuple[VersionedHash, ...] + """ + A tuple of objects that represent the versioned hashes of the blobs + included in the transaction. + """ + + +StandardTransaction = ( LegacyTransaction | AccessListTransaction | FeeMarketTransaction @@ -491,6 +821,15 @@ class SetCodeTransaction: | SetCodeTransaction ) """ +Union type representing transaction types authenticated by a single +ECDSA signature, i.e. every type except [`FrameTransaction`]. + +[`FrameTransaction`]: ref:ethereum.forks.bogota.transactions.FrameTransaction +""" + + +Transaction = StandardTransaction | FrameTransaction +""" Union type representing any valid transaction type. """ @@ -543,6 +882,8 @@ def encode_transaction(tx: Transaction) -> LegacyTransaction | Bytes: return b"\x03" + rlp.encode(tx) elif isinstance(tx, SetCodeTransaction): return b"\x04" + rlp.encode(tx) + elif isinstance(tx, FrameTransaction): + return b"\x06" + rlp.encode(tx) else: raise Exception(f"Unable to encode transaction of type {type(tx)}") @@ -568,6 +909,8 @@ def decode_transaction(tx: LegacyTransaction | Bytes) -> Transaction: return rlp.decode_to(BlobTransaction, tx[1:]) elif tx[0] == 4: return rlp.decode_to(SetCodeTransaction, tx[1:]) + elif tx[0] == 6: + return rlp.decode_to(FrameTransaction, tx[1:]) elif tx[0] >= 0xC0: assert tx[0] <= 0xFE return rlp.decode_to(LegacyTransaction, tx) @@ -577,7 +920,9 @@ def decode_transaction(tx: LegacyTransaction | Bytes) -> Transaction: return tx -def validate_transaction(tx: Transaction, sender: Address) -> IntrinsicGasCost: +def validate_transaction( + tx: StandardTransaction, sender: Address +) -> IntrinsicGasCost: """ Verifies a transaction. @@ -632,7 +977,7 @@ def validate_transaction(tx: Transaction, sender: Address) -> IntrinsicGasCost: def calculate_intrinsic_cost( - tx: Transaction, sender: Address + tx: StandardTransaction, sender: Address ) -> IntrinsicGasCost: """ Calculates the gas that is charged before execution is started. @@ -759,7 +1104,7 @@ def count_tokens_in_data(data: bytes) -> Uint: return num_zeros + num_non_zeros * Uint(4) -def chain_id(tx: Transaction) -> None | U64: +def chain_id(tx: StandardTransaction) -> None | U64: """ Extract the chain identifier from a transaction. See [EIP-155]. @@ -777,7 +1122,7 @@ def chain_id(tx: Transaction) -> None | U64: return tx.chain_id -def recover_sender(tx: Transaction) -> Address: +def recover_sender(tx: StandardTransaction) -> Address: """ Extracts the sender address from a transaction. @@ -1029,3 +1374,253 @@ def has_access_list( tx, AccessListCapableTransaction, ) + + +def resolve_frame_target(tx: FrameTransaction, frame: Frame) -> Address: + """ + Return the resolved target address of a frame. + + An empty frame target resolves to the transaction sender. + """ + if isinstance(frame.target, Bytes0): + return tx.sender + return frame.target + + +def is_expiry_verifier_frame(frame: Frame) -> bool: + """ + Return whether the frame is an expiry verifier frame, i.e. a + `VERIFY` frame targeting the [`EXPIRY_VERIFIER`] contract. + + [`EXPIRY_VERIFIER`]: ref:ethereum.forks.bogota.transactions.EXPIRY_VERIFIER + """ + return frame.mode == FRAME_MODE_VERIFY and frame.target == EXPIRY_VERIFIER + + +def validate_frame_transaction(tx: FrameTransaction) -> Uint: + """ + Verify the static constraints of a frame transaction and return its + total gas limit. + + The frame count, frame fields, and signature entry structure are + checked against the limits defined in [EIP-8141]. A + `FrameTransactionFormatError` is raised for any violation and a + `NonceOverflowError` is raised when the nonce exceeds the [EIP-2681] + limit. + + [EIP-2681]: https://eips.ethereum.org/EIPS/eip-2681 + [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 + """ + if len(tx.frames) == 0 or len(tx.frames) > MAX_FRAMES: + raise FrameTransactionFormatError( + "frame count must be greater than 0 and at most MAX_FRAMES" + ) + + for sig in tx.signatures: + if tx_signature_scheme_is_protocol_validated(sig): + if len(sig.signer) != 20: + raise FrameTransactionFormatError( + "signer must be a 20-byte address" + ) + elif sig.scheme == SIGNATURE_SCHEME_ARBITRARY: + if len(sig.signer) != 0: + raise FrameTransactionFormatError( + "arbitrary signature signer must be empty" + ) + else: + raise FrameTransactionFormatError("unknown signature scheme") + if len(sig.msg) == 32: + if sig.msg == b"\x00" * 32: + raise FrameTransactionFormatError( + "explicit zero digest is invalid" + ) + elif len(sig.msg) != 0: + raise FrameTransactionFormatError( + "signature msg must be empty or 32 bytes" + ) + + total_frame_gas = Uint(0) + expiry_verifier_frames = 0 + for i, frame in enumerate(tx.frames): + if frame.mode >= FRAME_MODE_COUNT: + raise FrameTransactionFormatError("unknown frame mode") + if frame.flags >= FRAME_FLAGS_LIMIT: + raise FrameTransactionFormatError("reserved frame flags set") + if frame.gas_limit > Uint(U64.MAX_VALUE): + raise FrameTransactionFormatError("frame gas limit too high") + if frame.mode != FRAME_MODE_SENDER and frame.value != 0: + raise FrameTransactionFormatError( + "non-zero value outside SENDER mode" + ) + total_frame_gas += frame.gas_limit + if total_frame_gas > Uint(U64.MAX_VALUE): + raise FrameTransactionFormatError("total frame gas too high") + + # An atomic batch must be terminated by a subsequent frame. + if frame.flags & ATOMIC_BATCH_FLAG and i + 1 >= len(tx.frames): + raise FrameTransactionFormatError( + "atomic batch flag set on last frame" + ) + + if is_expiry_verifier_frame(frame): + expiry_verifier_frames += 1 + if frame.flags != 0: + raise FrameTransactionFormatError( + "expiry verifier frame flags must be zero" + ) + if len(frame.data) != EXPIRY_DATA_LENGTH: + raise FrameTransactionFormatError( + "expiry verifier frame data must be 8 bytes" + ) + + if expiry_verifier_frames > 1: + raise FrameTransactionFormatError("multiple expiry verifier frames") + + if len(tx.blob_versioned_hashes) == 0 and tx.max_fee_per_blob_gas != 0: + raise FrameTransactionFormatError( + "max_fee_per_blob_gas must be zero without blobs" + ) + + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise NonceOverflowError("Nonce too high") + + return calculate_frame_transaction_gas_limit(tx) + + +def tx_signature_scheme_is_protocol_validated( + sig: TransactionSignature, +) -> bool: + """ + Return whether the signature entry uses a scheme that is + cryptographically validated by the protocol. + """ + return sig.scheme in ( + SIGNATURE_SCHEME_SECP256K1, + SIGNATURE_SCHEME_P256, + ) + + +def signature_verification_gas(sig: TransactionSignature) -> Uint: + """ + Return the gas charged for validating a single signature entry. + """ + if sig.scheme == SIGNATURE_SCHEME_SECP256K1: + return SECP256K1_SIGNATURE_VERIFICATION_GAS + if sig.scheme == SIGNATURE_SCHEME_P256: + return P256_SIGNATURE_VERIFICATION_GAS + assert sig.scheme == SIGNATURE_SCHEME_ARBITRARY + return Uint(0) + + +def calculate_frame_transaction_gas_limit(tx: FrameTransaction) -> Uint: + """ + Calculate the total gas limit of a frame transaction. + + The gas limit is the sum of the frame transaction intrinsic cost, + the per-frame cost, the [EIP-7623] calldata cost of the encoded + signature and frame lists, the signature verification cost, and the + gas limits of all frames. + + [EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 + """ + from .vm.gas import GasCosts + + signature_gas = Uint(0) + for sig in tx.signatures: + signature_gas += signature_verification_gas(sig) + + calldata_tokens = count_tokens_in_data( + rlp.encode(tx.signatures) + ) + count_tokens_in_data(rlp.encode(tx.frames)) + calldata_cost = calldata_tokens * GasCosts.TX_DATA_TOKEN_STANDARD + + total_frame_gas = Uint(0) + for frame in tx.frames: + total_frame_gas += frame.gas_limit + + return ( + FRAME_TX_INTRINSIC_COST + + ulen(tx.frames) * FRAME_TX_PER_FRAME_COST + + calldata_cost + + signature_gas + + total_frame_gas + ) + + +def compute_frame_signature_hash(tx: FrameTransaction) -> Hash32: + """ + Compute the canonical signature hash of a frame transaction. + + The raw `signature` bytes of every entry with an empty `msg` are + elided before hashing, since a signature over the canonical hash + cannot commit to its own bytes. + """ + elided_signatures = [] + for sig in tx.signatures: + if len(sig.msg) == 0: + elided_signatures.append(replace(sig, signature=Bytes(b""))) + else: + elided_signatures.append(sig) + + elided_tx = replace(tx, signatures=tuple(elided_signatures)) + return keccak256(b"\x06" + rlp.encode(elided_tx)) + + +def validate_frame_signature( + sig: TransactionSignature, sig_hash: Hash32 +) -> bool: + """ + Validate a single signature entry of a frame transaction. + + An empty `msg` authorizes the canonical signature hash; a 32-byte + `msg` authorizes that explicit digest. `SECP256K1` and `P256` + entries are cryptographically verified against their `signer` + address, while `ARBITRARY` entries are only structurally checked. + """ + if len(sig.msg) == 0: + msg = sig_hash + elif len(sig.msg) == 32: + if sig.msg == b"\x00" * 32: + return False + msg = Hash32(sig.msg) + else: + return False + + if sig.scheme == SIGNATURE_SCHEME_SECP256K1: + if len(sig.signature) != 65: + return False + v = U256(sig.signature[0]) + r = U256.from_be_bytes(sig.signature[1:33]) + s = U256.from_be_bytes(sig.signature[33:65]) + if v not in (U256(0), U256(1)): + return False + if U256(0) >= r or r >= SECP256K1N: + return False + if U256(0) >= s or s > SECP256K1N // U256(2): + return False + try: + public_key = secp256k1_recover(r, s, v, msg) + except InvalidSignatureError: + return False + return Bytes(sig.signer) == keccak256(public_key)[12:32] + + elif sig.scheme == SIGNATURE_SCHEME_P256: + if len(sig.signature) != 128: + return False + r = U256.from_be_bytes(sig.signature[0:32]) + s = U256.from_be_bytes(sig.signature[32:64]) + qx = U256.from_be_bytes(sig.signature[64:96]) + qy = U256.from_be_bytes(sig.signature[96:128]) + if Bytes(sig.signer) != keccak256(sig.signature[64:128])[12:32]: + return False + try: + secp256r1_verify(r, s, qx, qy, msg) + except (InvalidSignatureError, ValueError): + return False + return True + + elif sig.scheme == SIGNATURE_SCHEME_ARBITRARY: + return len(sig.signer) == 0 + + else: + return False diff --git a/src/ethereum/forks/bogota/utils/message.py b/src/ethereum/forks/bogota/utils/message.py index 4cef9d19264..2720586d9bf 100644 --- a/src/ethereum/forks/bogota/utils/message.py +++ b/src/ethereum/forks/bogota/utils/message.py @@ -18,7 +18,7 @@ from ethereum.state import Address from ..state_tracker import get_account, get_code -from ..transactions import Transaction +from ..transactions import StandardTransaction from ..vm import BlockEnvironment, Message, TransactionEnvironment from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from .address import compute_contract_address @@ -27,7 +27,7 @@ def prepare_message( block_env: BlockEnvironment, tx_env: TransactionEnvironment, - tx: Transaction, + tx: StandardTransaction, ) -> Message: """ Execute a transaction against the provided environment. diff --git a/src/ethereum/forks/bogota/vm/__init__.py b/src/ethereum/forks/bogota/vm/__init__.py index 27ea3398411..376df5c75bb 100644 --- a/src/ethereum/forks/bogota/vm/__init__.py +++ b/src/ethereum/forks/bogota/vm/__init__.py @@ -27,8 +27,20 @@ from ..block_access_lists import BlockAccessList, BlockAccessListBuilder from ..blocks import Log, Receipt, Withdrawal from ..fork_types import Authorization, StateGas, VersionedHash -from ..state_tracker import BlockState, TransactionState -from ..transactions import LegacyTransaction +from ..state_tracker import ( + BlockState, + TransactionState, + get_account, + increment_nonce, + set_account_balance, +) +from ..transactions import ( + APPROVE_EXECUTION, + APPROVE_PAYMENT, + APPROVE_SCOPE_MASK, + FrameTransaction, + LegacyTransaction, +) __all__ = ("Environment", "Evm", "Message") TRANSFER_TOPIC = keccak256(b"Transfer(address,address,uint256)") @@ -112,6 +124,180 @@ class BlockOutput: block_access_list: BlockAccessList = field(default_factory=list) +@final +@dataclass +class FrameApproval: + """ + A pending approval granted by the `APPROVE` instruction. + + Approvals accumulate on the [`Evm`] like logs: they propagate to the + parent call on success and are discarded when the granting call + reverts. They are applied to the [`FrameTransactionContext`][ctx] + once the frame completes successfully. + + [`Evm`]: ref:ethereum.forks.bogota.vm.Evm + [ctx]: ref:ethereum.forks.bogota.vm.FrameTransactionContext + """ + + scope: Uint + """ + The approval scope granted: a bitmask of [`APPROVE_PAYMENT`][pay] + and [`APPROVE_EXECUTION`][exe]. + + [pay]: ref:ethereum.forks.bogota.transactions.APPROVE_PAYMENT + [exe]: ref:ethereum.forks.bogota.transactions.APPROVE_EXECUTION + """ + + approver: Address + """ + The resolved target of the frame that granted the approval. Becomes + the payer when the scope includes payment approval. + """ + + +@final +@dataclass +class FrameTransactionContext: + """ + Transaction-scoped context of an executing frame transaction, as + defined in [EIP-8141]. Shared by all frames of the transaction. + + [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 + """ + + tx: FrameTransaction + """ + The frame transaction being executed. + """ + + sig_hash: Hash32 + """ + The canonical signature hash of the transaction. + """ + + max_cost: Uint + """ + The maximum cost of the transaction, collected from the payer upon + payment approval: the total gas limit priced at `max_fee_per_gas` + plus the blob fees priced at `max_fee_per_blob_gas`. + """ + + sender_approved: bool = False + """ + Whether a frame has approved execution on behalf of the sender. + """ + + payer: Optional[Address] = None + """ + The account that approved payment and was charged the maximum + transaction cost, or `None` while payment is unapproved. + """ + + current_frame_index: Uint = Uint(0) + """ + The index of the currently executing frame. + """ + + frame_statuses: List[Uint] = field(default_factory=list) + """ + Status codes of the frames executed so far. + """ + + +def apply_frame_approval( + frame_context: FrameTransactionContext, approval: FrameApproval +) -> None: + """ + Apply a committed approval to the frame transaction context. + + Parameters + ---------- + frame_context : + The context of the executing frame transaction. + approval : + The approval to apply. + + """ + if approval.scope & APPROVE_EXECUTION: + frame_context.sender_approved = True + if approval.scope & APPROVE_PAYMENT: + frame_context.payer = approval.approver + + +def attempt_frame_approval( + frame_context: FrameTransactionContext, + scope: Uint, + frame_flags: Uint, + resolved_target: Address, + sender_approved: bool, + payer: Optional[Address], + tx_state: TransactionState, +) -> Optional[FrameApproval]: + """ + Validate and perform an `APPROVE` of `scope`, returning the granted + approval or `None` when the request must revert the calling frame. + + On payment approval the sender's nonce is incremented and the + maximum transaction cost is collected from the resolved target. + These state changes are journaled by the calling EVM frame and roll + back together with the returned approval if that frame later + reverts. + + Parameters + ---------- + frame_context : + The context of the executing frame transaction. + scope : + The requested approval scope. + frame_flags : + The flags of the frame requesting approval; bits 0-1 hold the + allowed approval scope. + resolved_target : + The resolved target of the frame requesting approval. + sender_approved : + Whether execution approval is in effect, including pending + approvals of the calling frame. + payer : + The payment approver in effect, including pending approvals of + the calling frame. + tx_state : + The transaction state. + + Returns + ------- + approval : `Optional[FrameApproval]` + The granted approval, or `None` if the request is not allowed. + + """ + tx_sender = frame_context.tx.sender + allowed_scope = frame_flags & APPROVE_SCOPE_MASK + if scope == 0 or int(scope) & ~int(allowed_scope) != 0: + return None + + if scope & APPROVE_EXECUTION: + if sender_approved: + return None + if resolved_target != tx_sender: + return None + + if scope & APPROVE_PAYMENT: + if payer is not None: + return None + approver_balance = get_account(tx_state, resolved_target).balance + if Uint(approver_balance) < frame_context.max_cost: + return None + if not (sender_approved or scope & APPROVE_EXECUTION): + return None + increment_nonce(tx_state, tx_sender) + set_account_balance( + tx_state, + resolved_target, + U256(Uint(approver_balance) - frame_context.max_cost), + ) + + return FrameApproval(scope=scope, approver=resolved_target) + + @final @dataclass class TransactionEnvironment: @@ -134,6 +320,7 @@ class TransactionEnvironment: tx_hash: Optional[Hash32] intrinsic_regular_gas: Uint intrinsic_state_gas: Uint + frame_context: Optional[FrameTransactionContext] = None @final @@ -187,6 +374,53 @@ class Evm: accessed_storage_keys: Set[Tuple[Address, Bytes32]] regular_gas_used: Uint = Uint(0) state_gas_spilled: Uint = Uint(0) + approvals: Tuple[FrameApproval, ...] = () + + +def pending_frame_approval_state( + evm: Evm, +) -> Tuple[bool, Optional[Address]]: + """ + Return the approval state visible to the given EVM frame. + + Combines the committed approvals of the + [`FrameTransactionContext`][ctx] with the pending approvals + journaled along the call chain of `evm`, so that an `APPROVE` in a + child call is visible to later `APPROVE` checks in the same frame + while still rolling back if an enclosing call reverts. + + Parameters + ---------- + evm : + The current EVM frame. + + Returns + ------- + approval_state : `Tuple[bool, Optional[Address]]` + Whether execution is approved, and the payment approver if any. + + [ctx]: ref:ethereum.forks.bogota.vm.FrameTransactionContext + + """ + frame_context = evm.message.tx_env.frame_context + assert frame_context is not None + sender_approved = frame_context.sender_approved + payer = frame_context.payer + + lineage = [] + ancestor: Optional[Evm] = evm + while ancestor is not None: + lineage.append(ancestor) + ancestor = ancestor.message.parent_evm + + for ancestor in reversed(lineage): + for approval in ancestor.approvals: + if approval.scope & APPROVE_EXECUTION: + sender_approved = True + if approval.scope & APPROVE_PAYMENT: + payer = approval.approver + + return sender_approved, payer def credit_state_gas_refund(evm: Evm, amount: StateGas) -> None: @@ -233,6 +467,7 @@ def incorporate_child_on_success(evm: Evm, child_evm: Evm) -> None: evm.accessed_addresses.update(child_evm.accessed_addresses) evm.accessed_storage_keys.update(child_evm.accessed_storage_keys) evm.regular_gas_used += child_evm.regular_gas_used + evm.approvals += child_evm.approvals def refill_frame_state_gas(evm: Evm) -> None: diff --git a/src/ethereum/forks/bogota/vm/gas.py b/src/ethereum/forks/bogota/vm/gas.py index 3924eb86e7f..24580c1b7e9 100644 --- a/src/ethereum/forks/bogota/vm/gas.py +++ b/src/ethereum/forks/bogota/vm/gas.py @@ -22,7 +22,7 @@ from ..blocks import Header from ..fork_types import StateGas, StateGasPerByte -from ..transactions import BlobTransaction, Transaction +from ..transactions import BlobTransaction, FrameTransaction, Transaction from . import Evm from .exceptions import OutOfGasError @@ -212,11 +212,18 @@ class GasCosts: OPCODE_EXCHANGE: Final[Uint] = VERY_LOW OPCODE_TLOAD: Final[Uint] = Uint(100) OPCODE_TSTORE: Final[Uint] = Uint(100) + OPCODE_APPROVE: Final[Uint] = ZERO + OPCODE_TXPARAM: Final[Uint] = BASE + OPCODE_FRAMEDATALOAD: Final[Uint] = VERY_LOW + OPCODE_FRAMEPARAM: Final[Uint] = BASE + OPCODE_SIGPARAM: Final[Uint] = BASE # Dynamic Opcode Components OPCODE_RETURNDATACOPY_BASE: Final[Uint] = VERY_LOW OPCODE_RETURNDATACOPY_PER_WORD: Final[Uint] = Uint(3) OPCODE_CALLDATACOPY_BASE: Final[Uint] = VERY_LOW + OPCODE_FRAMEDATACOPY_BASE: Final[Uint] = VERY_LOW + OPCODE_SIGPARAM_COPY_BASE: Final[Uint] = VERY_LOW OPCODE_CODECOPY_BASE: Final[Uint] = VERY_LOW OPCODE_MCOPY_BASE: Final[Uint] = VERY_LOW OPCODE_MLOAD_BASE: Final[Uint] = VERY_LOW @@ -546,7 +553,7 @@ def calculate_total_blob_gas(tx: Transaction) -> U64: The total blob gas for the transaction. """ - if isinstance(tx, BlobTransaction): + if isinstance(tx, (BlobTransaction, FrameTransaction)): return GasCosts.PER_BLOB * U64(len(tx.blob_versioned_hashes)) else: return U64(0) diff --git a/src/ethereum/forks/bogota/vm/instructions/__init__.py b/src/ethereum/forks/bogota/vm/instructions/__init__.py index 06295ec86f1..a097fcfda79 100644 --- a/src/ethereum/forks/bogota/vm/instructions/__init__.py +++ b/src/ethereum/forks/bogota/vm/instructions/__init__.py @@ -208,6 +208,14 @@ class Ops(enum.Enum): LOG3 = 0xA3 LOG4 = 0xA4 + # Frame Transaction Operations + APPROVE = 0xAA + TXPARAM = 0xB0 + FRAMEDATALOAD = 0xB1 + FRAMEDATACOPY = 0xB2 + FRAMEPARAM = 0xB3 + SIGPARAM = 0xB4 + # System Operations CREATE = 0xF0 CALL = 0xF1 @@ -365,6 +373,12 @@ class Ops(enum.Enum): Ops.LOG2: log_instructions.log2, Ops.LOG3: log_instructions.log3, Ops.LOG4: log_instructions.log4, + Ops.APPROVE: system_instructions.approve, + Ops.TXPARAM: environment_instructions.txparam, + Ops.FRAMEDATALOAD: environment_instructions.framedataload, + Ops.FRAMEDATACOPY: environment_instructions.framedatacopy, + Ops.FRAMEPARAM: environment_instructions.frameparam, + Ops.SIGPARAM: environment_instructions.sigparam, Ops.CREATE: system_instructions.create, Ops.RETURN: system_instructions.return_, Ops.CALL: system_instructions.call, diff --git a/src/ethereum/forks/bogota/vm/instructions/environment.py b/src/ethereum/forks/bogota/vm/instructions/environment.py index 8a7e9ec1486..f0e5fb826a6 100644 --- a/src/ethereum/forks/bogota/vm/instructions/environment.py +++ b/src/ethereum/forks/bogota/vm/instructions/environment.py @@ -18,10 +18,17 @@ from ethereum.utils.numeric import ceil32 from ...state_tracker import get_account, get_code +from ...transactions import ( + APPROVE_SCOPE_MASK, + ATOMIC_BATCH_FLAG, + SIGNATURE_SCHEME_ARBITRARY, + resolve_frame_target, + tx_signature_scheme_is_protocol_validated, +) from ...utils.address import to_address_masked from ...vm.memory import buffer_read, memory_write -from .. import Evm -from ..exceptions import OutOfBoundsRead +from .. import Evm, FrameTransactionContext +from ..exceptions import InvalidParameter, OutOfBoundsRead from ..gas import ( GasCosts, calculate_blob_gas_price, @@ -609,3 +616,288 @@ def blob_base_fee(evm: Evm) -> None: # PROGRAM COUNTER evm.pc += Uint(1) + + +def active_frame_transaction_context(evm: Evm) -> FrameTransactionContext: + """ + Return the context of the executing frame transaction. + + An exceptional halt occurs when the current transaction is not a + frame transaction. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + frame_context = evm.message.tx_env.frame_context + if frame_context is None: + raise InvalidParameter("no frame transaction context") + return frame_context + + +def txparam(evm: Evm) -> None: + """ + Push a transaction-scoped parameter of the executing frame + transaction onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + param = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_TXPARAM) + + # OPERATION + frame_context = active_frame_transaction_context(evm) + tx = frame_context.tx + if param == U256(0x00): + value = U256(0x06) + elif param == U256(0x01): + value = U256(tx.nonce) + elif param == U256(0x02): + value = U256.from_be_bytes(tx.sender) + elif param == U256(0x03): + value = U256(tx.max_priority_fee_per_gas) + elif param == U256(0x04): + value = U256(tx.max_fee_per_gas) + elif param == U256(0x05): + value = tx.max_fee_per_blob_gas + elif param == U256(0x06): + value = U256(frame_context.max_cost) + elif param == U256(0x07): + value = U256(len(tx.blob_versioned_hashes)) + elif param == U256(0x08): + value = U256.from_be_bytes(frame_context.sig_hash) + elif param == U256(0x09): + value = U256(len(tx.frames)) + elif param == U256(0x0A): + value = U256(frame_context.current_frame_index) + elif param == U256(0x0B): + value = U256(len(tx.signatures)) + else: + raise InvalidParameter("undefined TXPARAM parameter") + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def framedataload(evm: Evm) -> None: + """ + Push a word (32 bytes) of the data of the chosen frame onto the + stack, with `CALLDATALOAD` semantics. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + start_index = pop(evm.stack) + frame_index = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_FRAMEDATALOAD) + + # OPERATION + frame_context = active_frame_transaction_context(evm) + if Uint(frame_index) >= ulen(frame_context.tx.frames): + raise OutOfBoundsRead("frame index out of bounds") + frame = frame_context.tx.frames[int(frame_index)] + value = buffer_read(frame.data, start_index, U256(32)) + push(evm.stack, U256.from_be_bytes(value)) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def framedatacopy(evm: Evm) -> None: + """ + Copy a portion of the data of the chosen frame to memory, with + `CALLDATACOPY` semantics. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + memory_start_index = pop(evm.stack) + data_start_index = pop(evm.stack) + size = pop(evm.stack) + frame_index = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GasCosts.OPCODE_COPY_PER_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GasCosts.OPCODE_FRAMEDATACOPY_BASE + + copy_gas_cost + + extend_memory.cost, + ) + + # OPERATION + frame_context = active_frame_transaction_context(evm) + if Uint(frame_index) >= ulen(frame_context.tx.frames): + raise OutOfBoundsRead("frame index out of bounds") + frame = frame_context.tx.frames[int(frame_index)] + evm.memory += b"\x00" * extend_memory.expand_by + value = buffer_read(frame.data, data_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def frameparam(evm: Evm) -> None: + """ + Push a frame-scoped parameter of the chosen frame onto the stack. + + Accessing the return status of the current frame or a future frame + results in an exceptional halt. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + frame_index = pop(evm.stack) + param = pop(evm.stack) + + # GAS + charge_gas(evm, GasCosts.OPCODE_FRAMEPARAM) + + # OPERATION + frame_context = active_frame_transaction_context(evm) + if Uint(frame_index) >= ulen(frame_context.tx.frames): + raise OutOfBoundsRead("frame index out of bounds") + frame = frame_context.tx.frames[int(frame_index)] + if param == U256(0x00): + value = U256.from_be_bytes( + resolve_frame_target(frame_context.tx, frame) + ) + elif param == U256(0x01): + value = U256(frame.gas_limit) + elif param == U256(0x02): + value = U256(frame.mode) + elif param == U256(0x03): + value = U256(frame.flags) + elif param == U256(0x04): + value = U256(len(frame.data)) + elif param == U256(0x05): + if Uint(frame_index) >= frame_context.current_frame_index: + raise OutOfBoundsRead("status of current or future frame") + value = U256(frame_context.frame_statuses[int(frame_index)]) + elif param == U256(0x06): + value = U256(frame.flags & APPROVE_SCOPE_MASK) + elif param == U256(0x07): + value = U256((frame.flags & ATOMIC_BATCH_FLAG) >> Uint(2)) + elif param == U256(0x08): + value = frame.value + else: + raise InvalidParameter("undefined FRAMEPARAM parameter") + push(evm.stack, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def sigparam(evm: Evm) -> None: + """ + Push signature-scoped metadata of the chosen signature entry onto + the stack, or copy the raw bytes of an `ARBITRARY` signature entry + to memory. + + The raw signature bytes of protocol-validated schemes are not + accessible from the EVM. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + signature_index = pop(evm.stack) + param = pop(evm.stack) + + if param == U256(0x04): + size = pop(evm.stack) + data_start_index = pop(evm.stack) + memory_start_index = pop(evm.stack) + + # GAS + words = ceil32(Uint(size)) // Uint(32) + copy_gas_cost = GasCosts.OPCODE_COPY_PER_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas( + evm, + GasCosts.OPCODE_SIGPARAM_COPY_BASE + + copy_gas_cost + + extend_memory.cost, + ) + + # OPERATION + frame_context = active_frame_transaction_context(evm) + if Uint(signature_index) >= ulen(frame_context.tx.signatures): + raise OutOfBoundsRead("signature index out of bounds") + sig = frame_context.tx.signatures[int(signature_index)] + if sig.scheme != SIGNATURE_SCHEME_ARBITRARY: + raise InvalidParameter( + "signature bytes of protocol-validated schemes are not " + "accessible" + ) + evm.memory += b"\x00" * extend_memory.expand_by + value = buffer_read(sig.signature, data_start_index, size) + memory_write(evm.memory, memory_start_index, value) + + # PROGRAM COUNTER + evm.pc += Uint(1) + return + + # GAS + charge_gas(evm, GasCosts.OPCODE_SIGPARAM) + + # OPERATION + frame_context = active_frame_transaction_context(evm) + if Uint(signature_index) >= ulen(frame_context.tx.signatures): + raise OutOfBoundsRead("signature index out of bounds") + sig = frame_context.tx.signatures[int(signature_index)] + if param == U256(0x00): + if not tx_signature_scheme_is_protocol_validated(sig): + raise InvalidParameter( + "arbitrary signature entries have no effective signer" + ) + result = U256.from_be_bytes(sig.signer) + elif param == U256(0x01): + result = U256(sig.scheme) + elif param == U256(0x02): + if len(sig.msg) == 0: + result = U256(0) + else: + result = U256.from_be_bytes(sig.msg) + elif param == U256(0x03): + result = U256(len(sig.signature)) + else: + raise InvalidParameter("undefined SIGPARAM parameter") + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/bogota/vm/instructions/system.py b/src/ethereum/forks/bogota/vm/instructions/system.py index 9d4e4fa815d..7e1a0ee96d0 100644 --- a/src/ethereum/forks/bogota/vm/instructions/system.py +++ b/src/ethereum/forks/bogota/vm/instructions/system.py @@ -29,6 +29,7 @@ is_account_alive, move_ether, ) +from ...transactions import resolve_frame_target from ...utils.address import ( compute_contract_address, compute_create2_contract_address, @@ -41,12 +42,19 @@ CALL_SUCCESS, Evm, Message, + attempt_frame_approval, credit_state_gas_refund, emit_transfer_log, incorporate_child_on_error, incorporate_child_on_success, + pending_frame_approval_state, +) +from ..exceptions import ( + InvalidParameter, + OutOfGasError, + Revert, + WriteInStaticContext, ) -from ..exceptions import OutOfGasError, Revert, WriteInStaticContext from ..gas import ( GasCosts, StateGasCosts, @@ -274,6 +282,69 @@ def create2(evm: Evm) -> None: evm.pc += Uint(1) +def approve(evm: Evm) -> None: + """ + Exit the current call successfully while updating the + transaction-scoped approval context of the executing frame + transaction, as defined in [EIP-8141]. + + The instruction reverts unless the executing account is the + resolved target of the current frame and the requested scope is + allowed by the frame's flags. On payment approval the sender's + nonce is incremented and the maximum transaction cost is collected + from the resolved target. + + [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 + """ + # STACK + memory_start_position = pop(evm.stack) + memory_size = pop(evm.stack) + scope = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_position, memory_size)] + ) + charge_gas(evm, GasCosts.OPCODE_APPROVE + extend_memory.cost) + + # OPERATION + frame_context = evm.message.tx_env.frame_context + if frame_context is None: + raise InvalidParameter("no frame transaction context") + tx = frame_context.tx + frame = tx.frames[int(frame_context.current_frame_index)] + resolved_target = resolve_frame_target(tx, frame) + + if evm.message.current_target != resolved_target: + evm.output = Bytes(b"") + raise Revert("APPROVE outside the frame's resolved target") + + sender_approved, payer = pending_frame_approval_state(evm) + approval = attempt_frame_approval( + frame_context=frame_context, + scope=Uint(scope), + frame_flags=frame.flags, + resolved_target=resolved_target, + sender_approved=sender_approved, + payer=payer, + tx_state=evm.message.tx_env.state, + ) + if approval is None: + evm.output = Bytes(b"") + raise Revert("approval not granted") + + evm.approvals += (approval,) + evm.memory += b"\x00" * extend_memory.expand_by + evm.output = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + + evm.running = False + + # PROGRAM COUNTER + pass + + def return_(evm: Evm) -> None: """ Halts execution returning output data. diff --git a/src/ethereum/forks/bogota/vm/interpreter.py b/src/ethereum/forks/bogota/vm/interpreter.py index 9646bd0eec1..2fe3d34c022 100644 --- a/src/ethereum/forks/bogota/vm/interpreter.py +++ b/src/ethereum/forks/bogota/vm/interpreter.py @@ -56,6 +56,7 @@ from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from . import ( Evm, + FrameApproval, emit_transfer_log, frame_state_gas_used, refill_frame_state_gas, @@ -99,6 +100,8 @@ class MessageCallOutput: matches the receipt `cumulative_gas_used`. 10. `created_target_alive`: Whether a top-level creation transaction targeted an already-existent account. + 11. `approvals`: Approvals granted by the `APPROVE` + instruction that survived until the end of the call. """ gas_left: Uint @@ -112,6 +115,7 @@ class MessageCallOutput: state_gas_used: int state_refund: Uint created_target_alive: bool + approvals: Tuple[FrameApproval, ...] = () def process_message_call(message: Message) -> MessageCallOutput: @@ -172,10 +176,12 @@ def process_message_call(message: Message) -> MessageCallOutput: if evm.error: logs: Tuple[Log, ...] = () accounts_to_delete = set() + approvals: Tuple[FrameApproval, ...] = () else: logs = evm.logs accounts_to_delete = evm.accounts_to_delete refund_counter += U256(evm.refund_counter) + approvals = evm.approvals tx_end = TransactionEnd( int(message.gas) - int(evm.gas_left), evm.output, evm.error @@ -194,6 +200,7 @@ def process_message_call(message: Message) -> MessageCallOutput: state_gas_used=frame_state_gas_used(evm), state_refund=state_refund, created_target_alive=target_alive, + approvals=approvals, ) diff --git a/vulture_whitelist.py b/vulture_whitelist.py index ffd8e3992bd..dc1b1e7dde9 100644 --- a/vulture_whitelist.py +++ b/vulture_whitelist.py @@ -11,6 +11,10 @@ from ethereum.ethash import * from ethereum.fork_criteria import Unscheduled +from ethereum.forks.bogota.transactions import ( + EXPIRY_VERIFIER_CODE, + FRAME_MODE_DEFAULT, +) from ethereum.trace import EvmTracer from ethereum.utils.hexadecimal import hex_to_bytes256 from ethereum_optimized.state_db import State @@ -47,6 +51,10 @@ # src/ethereum/fork_criteria.py Unscheduled +# src/ethereum/forks/bogota/transactions.py +EXPIRY_VERIFIER_CODE +FRAME_MODE_DEFAULT + # src/ethereum/ethash.py ethash.generate_dataset From ac6f51e3ef343daa47fb0cfa10568e736fbded09 Mon Sep 17 00:00:00 2001 From: lightclient Date: Mon, 6 Jul 2026 06:33:25 -0600 Subject: [PATCH 3/9] eip-8141: enforce EIP-7623 calldata floor on frame transactions Mirror ordinary calldata pricing: the encoded signature and frame lists are charged at the standard token cost inside the derived gas limit, and the final gas used is floored at the EIP-7976 floor token cost over all encoded bytes plus the frame transaction intrinsic cost. A transaction whose floor exceeds its derived gas limit is invalid. --- src/ethereum/forks/bogota/fork.py | 10 ++++++++- src/ethereum/forks/bogota/transactions.py | 27 ++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/ethereum/forks/bogota/fork.py b/src/ethereum/forks/bogota/fork.py index 40ba1ba28be..93ffd8981ad 100644 --- a/src/ethereum/forks/bogota/fork.py +++ b/src/ethereum/forks/bogota/fork.py @@ -116,6 +116,7 @@ SetCodeTransaction, StandardTransaction, Transaction, + calculate_frame_transaction_calldata_floor, chain_id, compute_frame_signature_hash, decode_transaction, @@ -1775,7 +1776,14 @@ def process_frame_transaction( tx_gas_refund = min( tx_gas_used_before_refund // Uint(5), Uint(refund_counter) ) - tx_gas_used = tx_gas_used_before_refund - tx_gas_refund + tx_gas_used_after_refund = tx_gas_used_before_refund - tx_gas_refund + + # Transactions with less gas used than the floor pay at the floor + # cost. + tx_gas_used = max( + tx_gas_used_after_refund, + calculate_frame_transaction_calldata_floor(tx), + ) # Refund the payer everything beyond the actual transaction fee. actual_fee = tx_gas_used * effective_gas_price + blob_gas_fee diff --git a/src/ethereum/forks/bogota/transactions.py b/src/ethereum/forks/bogota/transactions.py index 28776b950b4..3fbf63e0d86 100644 --- a/src/ethereum/forks/bogota/transactions.py +++ b/src/ethereum/forks/bogota/transactions.py @@ -1484,7 +1484,11 @@ def validate_frame_transaction(tx: FrameTransaction) -> Uint: if U256(tx.nonce) >= U256(U64.MAX_VALUE): raise NonceOverflowError("Nonce too high") - return calculate_frame_transaction_gas_limit(tx) + tx_gas_limit = calculate_frame_transaction_gas_limit(tx) + if calculate_frame_transaction_calldata_floor(tx) > tx_gas_limit: + raise InsufficientTransactionGasError("Insufficient calldata floor") + + return tx_gas_limit def tx_signature_scheme_is_protocol_validated( @@ -1547,6 +1551,27 @@ def calculate_frame_transaction_gas_limit(tx: FrameTransaction) -> Uint: ) +def calculate_frame_transaction_calldata_floor(tx: FrameTransaction) -> Uint: + """ + Calculate the minimum gas cost of a frame transaction based on the + size of the encoded signature and frame lists, per [EIP-7623] and + [EIP-7976]. Like ordinary calldata, every encoded byte counts as a + standard token and is priced at the floor token cost. + + [EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 + [EIP-7976]: https://eips.ethereum.org/EIPS/eip-7976 + """ + from .vm.gas import GasCosts + + floor_tokens = ( + ulen(rlp.encode(tx.signatures)) + ulen(rlp.encode(tx.frames)) + ) * GasCosts.TX_DATA_TOKEN_STANDARD + + return ( + floor_tokens * GasCosts.TX_DATA_TOKEN_FLOOR + FRAME_TX_INTRINSIC_COST + ) + + def compute_frame_signature_hash(tx: FrameTransaction) -> Hash32: """ Compute the canonical signature hash of a frame transaction. From 0065380889aca47f7940d245cea35282451e087d Mon Sep 17 00:00:00 2001 From: lightclient Date: Mon, 6 Jul 2026 07:09:30 -0600 Subject: [PATCH 4/9] feat(testing): support frame transactions in test framework and t8n Adds EIP-8141 frame transaction support to execution_testing and the EELS t8n: - Frame and FrameSignature test types; Transaction type 6 handling with the canonical signature hash (empty-msg signature entries elided) and automatic signing of secp256k1 entries from the sender or per-entry secret keys - Bogota fork (Amsterdam + tx type 6) and Amsterdam->Bogota transition - frame transaction fixture types, including the EIP-8141 receipt payload ([cumulative_gas_used, payer, [status, gas_used, logs]]) so fixture receipt roots re-encode correctly - APPROVE/TXPARAM/FRAMEDATALOAD/FRAMEDATACOPY/FRAMEPARAM/SIGPARAM opcodes for bytecode construction - type-6 transaction exceptions mapped to the EELS error names - t8n: frame transaction JSON loading (frames/signatures/sender) and frame receipt output (payer, frameReceipts) --- .../testing/src/execution_testing/__init__.py | 6 + .../client_clis/clis/execution_specs.py | 9 + .../exceptions/exceptions/transaction.py | 17 ++ .../execution_testing/fixtures/blockchain.py | 4 + .../src/execution_testing/fixtures/common.py | 46 ++++ .../src/execution_testing/fixtures/state.py | 4 + .../src/execution_testing/forks/__init__.py | 4 + .../execution_testing/forks/forks/forks.py | 9 + .../forks/forks/transition.py | 8 + .../forks/tests/test_forks.py | 5 +- .../execution_testing/test_types/__init__.py | 7 +- .../test_types/receipt_types.py | 13 ++ .../test_types/transaction_types.py | 209 +++++++++++++++++- .../src/execution_testing/vm/opcodes.py | 199 +++++++++++++++++ .../evm_tools/loaders/fork_loader.py | 16 ++ .../evm_tools/loaders/transaction_loader.py | 48 +++- .../evm_tools/t8n/t8n_types.py | 49 ++++ vulture_whitelist.py | 6 + 18 files changed, 650 insertions(+), 9 deletions(-) diff --git a/packages/testing/src/execution_testing/__init__.py b/packages/testing/src/execution_testing/__init__.py index 87e87352186..afa5afb77a7 100644 --- a/packages/testing/src/execution_testing/__init__.py +++ b/packages/testing/src/execution_testing/__init__.py @@ -71,6 +71,9 @@ DepositRequest, Environment, FeeSystemContractRequest, + Frame, + FrameReceipt, + FrameSignature, NetworkWrappedTransaction, Removable, Requests, @@ -175,6 +178,9 @@ "EIPChecklist", "EngineAPIError", "Environment", + "Frame", + "FrameReceipt", + "FrameSignature", "EOA", "FeeSystemContractRequest", "FixedIterationsBytecode", diff --git a/packages/testing/src/execution_testing/client_clis/clis/execution_specs.py b/packages/testing/src/execution_testing/client_clis/clis/execution_specs.py index be3b9939a1a..1d180fa44a9 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/execution_specs.py +++ b/packages/testing/src/execution_testing/client_clis/clis/execution_specs.py @@ -237,6 +237,15 @@ class ExecutionSpecsExceptionMapper(ExceptionMapper): "Block access list exceeds gas limit" ), TransactionException.LOG_MISMATCH: "LogMismatchError", + TransactionException.TYPE_6_INVALID_FRAME_FORMAT: ( + "FrameTransactionFormatError" + ), + TransactionException.TYPE_6_INVALID_SIGNATURE: ( + "FrameTransactionSignatureError" + ), + TransactionException.TYPE_6_INVALID_FRAME_EXECUTION: ( + "FrameTransactionExecutionError" + ), } mapping_regex: ClassVar[Dict[ExceptionBase, str]] = { # Temporary solution for issue #1981. diff --git a/packages/testing/src/execution_testing/exceptions/exceptions/transaction.py b/packages/testing/src/execution_testing/exceptions/exceptions/transaction.py index 286a71e188d..1934846ab8e 100644 --- a/packages/testing/src/execution_testing/exceptions/exceptions/transaction.py +++ b/packages/testing/src/execution_testing/exceptions/exceptions/transaction.py @@ -194,5 +194,22 @@ class TransactionException(ExceptionBase): """ TYPE_4_TX_PRE_FORK = auto() """Transaction type 4 included before activation fork.""" + TYPE_6_INVALID_FRAME_FORMAT = auto() + """ + Transaction is type 6, but violates a static frame transaction + constraint (frame count, mode, flags, value, signature entry + structure, expiry verifier frame shape, blob fields, etc.). + """ + TYPE_6_INVALID_SIGNATURE = auto() + """ + Transaction is type 6, but a signature entry failed protocol + validation. + """ + TYPE_6_INVALID_FRAME_EXECUTION = auto() + """ + Transaction is type 6, but frame execution invalidated it (a SENDER + frame ran before execution approval, a VERIFY frame reverted, or no + frame approved gas payment). + """ LOG_MISMATCH = auto() """Transaction receipt logs do not match expected logs.""" diff --git a/packages/testing/src/execution_testing/fixtures/blockchain.py b/packages/testing/src/execution_testing/fixtures/blockchain.py index edd819ac30b..7e5b895d1dc 100644 --- a/packages/testing/src/execution_testing/fixtures/blockchain.py +++ b/packages/testing/src/execution_testing/fixtures/blockchain.py @@ -73,6 +73,8 @@ from .common import ( FixtureAuthorizationTuple, FixtureBlobSchedule, + FixtureFrame, + FixtureFrameSignature, FixtureTransactionReceipt, ) @@ -692,6 +694,8 @@ class FixtureTransaction( authorization_list: List[FixtureAuthorizationTuple] | None = None initcodes: List[Bytes] | None = None + frames: List[FixtureFrame] | None = None + signatures: List[FixtureFrameSignature] | None = None @classmethod def from_transaction(cls, tx: Transaction) -> Self: diff --git a/packages/testing/src/execution_testing/fixtures/common.py b/packages/testing/src/execution_testing/fixtures/common.py index e56cb5a8b4c..37b0e242000 100644 --- a/packages/testing/src/execution_testing/fixtures/common.py +++ b/packages/testing/src/execution_testing/fixtures/common.py @@ -27,6 +27,8 @@ ) from execution_testing.test_types.transaction_types import ( AuthorizationTupleGeneric, + FrameGeneric, + FrameSignatureGeneric, Transaction, ) @@ -104,6 +106,22 @@ def sign(self) -> None: return +class FixtureFrame(FrameGeneric[ZeroPaddedHexNumber]): + """Fixture variant of the EIP-8141 Frame type.""" + + # Allow extra fields: FixtureFrame is constructed from Frame via + # model_dump(), which may include extra fields. + model_config = CamelModel.model_config | {"extra": "ignore"} + + +class FixtureFrameSignature(FrameSignatureGeneric[ZeroPaddedHexNumber]): + """Fixture variant of the EIP-8141 signature entry type.""" + + # Allow extra fields: FixtureFrameSignature is constructed from + # FrameSignature via model_dump(), which may include extra fields. + model_config = CamelModel.model_config | {"extra": "ignore"} + + class FixtureTransactionLog(CamelModel, RLPSerializable): """Fixture variant of the TransactionLog type.""" @@ -126,6 +144,22 @@ class FixtureReceiptDelegation(ReceiptDelegation): nonce: ZeroPaddedHexNumber +class FixtureFrameReceipt(CamelModel, RLPSerializable): + """Fixture variant of the EIP-8141 FrameReceipt type.""" + + model_config = CamelModel.model_config | {"extra": "ignore"} + + status: ZeroPaddedHexNumber + gas_used: ZeroPaddedHexNumber + logs: List[FixtureTransactionLog] + + rlp_fields: ClassVar[List[str]] = [ + "status", + "gas_used", + "logs", + ] + + class FixtureTransactionReceipt(CamelModel, RLPSerializable): """Fixture variant of the TransactionReceipt type.""" @@ -137,6 +171,9 @@ class FixtureTransactionReceipt(CamelModel, RLPSerializable): post_state: Hash | None = None status: bool | None = None + payer: Address | None = None + frame_receipts: List[FixtureFrameReceipt] | None = None + rlp_fields: ClassVar[List[str]] = [ "post_state", "status", @@ -146,6 +183,15 @@ class FixtureTransactionReceipt(CamelModel, RLPSerializable): ] rlp_exclude_none: ClassVar[bool] = True + def get_rlp_fields(self) -> List[str]: + """ + Return the RLP field list, using the EIP-8141 frame receipt + payload for frame transactions. + """ + if self.payer is not None: + return ["cumulative_gas_used", "payer", "frame_receipts"] + return self.rlp_fields + @model_validator(mode="before") @classmethod def _drop_computed_fields(cls, data: Any) -> Any: diff --git a/packages/testing/src/execution_testing/fixtures/state.py b/packages/testing/src/execution_testing/fixtures/state.py index 1dc9043bfc2..2ddb01eecac 100644 --- a/packages/testing/src/execution_testing/fixtures/state.py +++ b/packages/testing/src/execution_testing/fixtures/state.py @@ -25,6 +25,8 @@ from .common import ( FixtureAuthorizationTuple, FixtureBlobSchedule, + FixtureFrame, + FixtureFrameSignature, FixtureTransactionReceipt, ) @@ -57,6 +59,8 @@ class FixtureTransaction(TransactionFixtureConverter): access_lists: List[List[AccessList] | None] | None = None authorization_list: List[FixtureAuthorizationTuple] | None = None initcodes: List[Bytes] | None = None + frames: List[FixtureFrame] | None = None + signatures: List[FixtureFrameSignature] | None = None max_fee_per_blob_gas: ZeroPaddedHexNumber | None = None blob_versioned_hashes: Sequence[Hash] | None = None sender: Address | None = None diff --git a/packages/testing/src/execution_testing/forks/__init__.py b/packages/testing/src/execution_testing/forks/__init__.py index cd333c559e8..94a88793205 100644 --- a/packages/testing/src/execution_testing/forks/__init__.py +++ b/packages/testing/src/execution_testing/forks/__init__.py @@ -10,6 +10,7 @@ Amsterdam, ArrowGlacier, Berlin, + Bogota, Byzantium, Cancun, Constantinople, @@ -28,6 +29,7 @@ TangerineWhistle, ) from .forks.transition import ( + AmsterdamToBogotaAtTime15k, BerlinToLondonAt5, BPO1ToBPO2AtTime15k, BPO2ToAmsterdamAtTime15k, @@ -90,8 +92,10 @@ "TransitionForkOrNoneAdapter", "RefundTypes", "Amsterdam", + "AmsterdamToBogotaAtTime15k", "ArrowGlacier", "Berlin", + "Bogota", "BerlinToLondonAt5", "Byzantium", "Constantinople", diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index 168a4b19e2b..833e112bdb4 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1645,3 +1645,12 @@ class Amsterdam( # live on mainnet. pass + + +class Bogota(Amsterdam, deployed=False): + """Bogota fork.""" + + @classmethod + def tx_types(cls) -> List[int]: + """At Bogota, frame transactions (type 6) are introduced.""" + return super(Bogota, cls).tx_types() + [6] diff --git a/packages/testing/src/execution_testing/forks/forks/transition.py b/packages/testing/src/execution_testing/forks/forks/transition.py index ac3e50786d4..8286830a165 100644 --- a/packages/testing/src/execution_testing/forks/forks/transition.py +++ b/packages/testing/src/execution_testing/forks/forks/transition.py @@ -8,6 +8,7 @@ BPO4, Amsterdam, Berlin, + Bogota, Cancun, London, Osaka, @@ -78,6 +79,13 @@ class BPO2ToAmsterdamAtTime15k(TransitionBaseClass): pass +@transition_fork(to_fork=Bogota, from_fork=Amsterdam, at_timestamp=15_000) +class AmsterdamToBogotaAtTime15k(TransitionBaseClass): + """Amsterdam to Bogota transition at Timestamp 15k.""" + + pass + + @transition_fork(to_fork=BPO3, from_fork=BPO2, at_timestamp=15_000) class BPO2ToBPO3AtTime15k(TransitionBaseClass): """BPO2 to BPO3 transition at Timestamp 15k.""" diff --git a/packages/testing/src/execution_testing/forks/tests/test_forks.py b/packages/testing/src/execution_testing/forks/tests/test_forks.py index c9c06f0a7b4..1856d56a573 100644 --- a/packages/testing/src/execution_testing/forks/tests/test_forks.py +++ b/packages/testing/src/execution_testing/forks/tests/test_forks.py @@ -17,6 +17,7 @@ BPO5, Amsterdam, Berlin, + Bogota, Cancun, Frontier, Homestead, @@ -57,8 +58,8 @@ FIRST_DEPLOYED = Frontier LAST_DEPLOYED = Osaka -LAST_DEVELOPMENT = Amsterdam -DEVELOPMENT_FORKS = [Amsterdam] +LAST_DEVELOPMENT = Bogota +DEVELOPMENT_FORKS = [Amsterdam, Bogota] def test_transition_forks() -> None: diff --git a/packages/testing/src/execution_testing/test_types/__init__.py b/packages/testing/src/execution_testing/test_types/__init__.py index ffb82b59196..871eca48c67 100644 --- a/packages/testing/src/execution_testing/test_types/__init__.py +++ b/packages/testing/src/execution_testing/test_types/__init__.py @@ -33,7 +33,7 @@ eoa_from_hash, ) from .phase_manager import TestPhase, TestPhaseManager -from .receipt_types import TransactionLog, TransactionReceipt +from .receipt_types import FrameReceipt, TransactionLog, TransactionReceipt from .request_types import ( BuilderDepositRequest, BuilderExitRequest, @@ -53,6 +53,8 @@ ) from .transaction_types import ( AuthorizationTuple, + Frame, + FrameSignature, NetworkWrappedTransaction, Transaction, TransactionDefaults, @@ -86,6 +88,9 @@ "Environment", "EnvironmentDefaults", "EOA", + "Frame", + "FrameReceipt", + "FrameSignature", "FeeSystemContractRequest", "NetworkWrappedTransaction", "Removable", diff --git a/packages/testing/src/execution_testing/test_types/receipt_types.py b/packages/testing/src/execution_testing/test_types/receipt_types.py index 55e94498350..143e6e063bf 100644 --- a/packages/testing/src/execution_testing/test_types/receipt_types.py +++ b/packages/testing/src/execution_testing/test_types/receipt_types.py @@ -37,6 +37,17 @@ class ReceiptDelegation(CamelModel): target: Address +class FrameReceipt(CamelModel): + """ + Per-frame receipt of an + [EIP-8141](https://eips.ethereum.org/EIPS/eip-8141) frame transaction. + """ + + status: HexNumber | None = None + gas_used: HexNumber | None = None + logs: List[TransactionLog] | None = None + + class TransactionReceipt(CamelModel): """Transaction receipt.""" @@ -86,3 +97,5 @@ def strip_extra_fields(cls, data: Any) -> Any: blob_gas_used: HexNumber | None = None blob_gas_price: HexNumber | None = None delegations: List[ReceiptDelegation] | None = None + payer: Address | None = None + frame_receipts: List[FrameReceipt] | None = None diff --git a/packages/testing/src/execution_testing/test_types/transaction_types.py b/packages/testing/src/execution_testing/test_types/transaction_types.py index 4b808e08b16..c788d5048e3 100644 --- a/packages/testing/src/execution_testing/test_types/transaction_types.py +++ b/packages/testing/src/execution_testing/test_types/transaction_types.py @@ -55,6 +55,7 @@ class TransactionType(IntEnum): BASE_FEE = 2 BLOB_TRANSACTION = 3 SET_CODE = 4 + FRAME = 6 @dataclass @@ -186,6 +187,80 @@ def sign(self: "AuthorizationTuple") -> None: pass +class FrameGeneric(CamelModel, Generic[NumberBoundTypeVar], RLPSerializable): + """ + Frame within an [EIP-8141](https://eips.ethereum.org/EIPS/eip-8141) + frame transaction. + """ + + mode: NumberBoundTypeVar = Field(0) # type: ignore + flags: NumberBoundTypeVar = Field(0) # type: ignore + target: Address | None = None + gas_limit: NumberBoundTypeVar = Field(0) # type: ignore + value: NumberBoundTypeVar = Field(0) # type: ignore + data: Bytes = Field(Bytes(b"")) + + rlp_fields: ClassVar[List[str]] = [ + "mode", + "flags", + "target", + "gas_limit", + "value", + "data", + ] + + +class Frame(FrameGeneric[HexNumber]): + """Frame within an EIP-8141 frame transaction (test authoring).""" + + pass + + +class FrameSignatureGeneric( + CamelModel, Generic[NumberBoundTypeVar], RLPSerializable +): + """ + Signature entry within an + [EIP-8141](https://eips.ethereum.org/EIPS/eip-8141) frame transaction. + """ + + scheme: NumberBoundTypeVar = Field(0) # type: ignore + signer: Bytes = Field(Bytes(b"")) + msg: Bytes = Field(Bytes(b"")) + signature: Bytes = Field(Bytes(b"")) + + rlp_fields: ClassVar[List[str]] = [ + "scheme", + "signer", + "msg", + "signature", + ] + + +class FrameSignature(FrameSignatureGeneric[HexNumber]): + """ + Signature entry within an EIP-8141 frame transaction (test authoring). + + When `secret_key` is set (or the entry's `signer` matches the + transaction sender), the raw `signature` bytes are filled in + automatically when the transaction is signed: entries with an + explicit 32-byte `msg` sign that digest, while entries with an empty + `msg` sign the canonical transaction signature hash. + """ + + secret_key: Hash | None = Field(None, exclude=True) + + def signed_over(self, digest: bytes, key: Hash) -> None: + """Fill the raw signature bytes by signing `digest` with `key`.""" + signature_bytes = PrivateKey(secret=key).sign_recoverable( + digest, hasher=None + ) + # EIP-8141 secp256k1 encoding: v (1 byte) || r || s, v in {0, 1}. + self.signature = Bytes( + bytes([signature_bytes[64]]) + signature_bytes[0:64] + ) + + class TransactionGeneric(BaseModel, Generic[NumberBoundTypeVar]): """ Generic transaction type used as a parent for Transaction and @@ -332,6 +407,9 @@ def treat_none_gas_limit_as_unset(cls, data: Any) -> Any: initcodes: List[Bytes] | None = None + frames: List[Frame] | None = None + signatures: List[FrameSignature] | None = None + secret_key: Hash | None = None error: List[TransactionException] | TransactionException | None = Field( None, exclude=True @@ -418,7 +496,9 @@ def model_post_init(self, __context: Any) -> None: if "ty" not in self.model_fields_set: # Try to deduce transaction type from included fields - if self.initcodes is not None: + if self.frames is not None: + self.ty = HexNumber(6) + elif self.initcodes is not None: self.ty = HexNumber(6) elif self.authorization_list is not None: self.ty = HexNumber(4) @@ -440,7 +520,13 @@ def model_post_init(self, __context: Any) -> None: if "v" in self.model_fields_set and self.secret_key is not None: raise Transaction.InvalidSignaturePrivateKeyError() - if "v" not in self.model_fields_set and self.secret_key is None: + if self.frames is not None: + # EIP-8141: Frame transactions carry an explicit sender and + # a signature list instead of a single v/r/s signature. + assert self.sender is not None, ( + "frame transactions require an explicit sender" + ) + elif "v" not in self.model_fields_set and self.secret_key is None: if self.sender is not None: self.secret_key = self.sender.key else: @@ -481,7 +567,13 @@ def model_post_init(self, __context: Any) -> None: if self.ty == 3 and self.max_fee_per_blob_gas is None: self.max_fee_per_blob_gas = HexNumber(1) self.model_fields_set.remove("max_fee_per_blob_gas") - if self.ty != 3: + if self.frames is not None: + # EIP-8141: Frame transactions always carry blob fields. + if self.blob_versioned_hashes is None: + self.blob_versioned_hashes = [] + if self.max_fee_per_blob_gas is None: + self.max_fee_per_blob_gas = HexNumber(0) + elif self.ty != 3: assert self.blob_versioned_hashes is None, ( "blob_versioned_hashes must be None" ) @@ -496,10 +588,12 @@ def model_post_init(self, __context: Any) -> None: "authorization_list must be None" ) - if self.ty == 6 and self.initcodes is None: + if self.ty == 6 and self.frames is None and self.initcodes is None: self.initcodes = [] if self.ty != 6: assert self.initcodes is None, "initcodes must be None" + assert self.frames is None, "frames must be None" + assert self.signatures is None, "signatures must be None" if "nonce" not in self.model_fields_set and self.sender is not None: self.nonce = HexNumber(self.sender.get_nonce()) @@ -531,8 +625,87 @@ def signature_bytes(self) -> Bytes: + bytes([v]) ) + @property + def signing_signatures(self) -> List[FrameSignature]: + """ + Return the signature entries as included in the canonical frame + transaction signature hash: entries with an empty `msg` have + their raw `signature` bytes elided. + """ + assert self.signatures is not None + return [ + sig.model_copy(update={"signature": Bytes(b"")}) + if len(sig.msg) == 0 + else sig + for sig in self.signatures + ] + + def _sign_frame_signatures(self) -> None: + """ + Fill in the raw signature bytes of the frame transaction's + signature entries. + + A missing signature list defaults to a single secp256k1 entry + from the sender over the canonical signature hash. Entries with + an explicit 32-byte `msg` are signed first since their raw bytes + are committed to by the canonical hash. + """ + assert self.frames is not None + if self.signatures is None: + assert self.sender is not None + if getattr(self.sender, "key", None) is not None: + # EOA sender: default to a single secp256k1 entry over + # the canonical signature hash, as consumed by the + # default code. + self.signatures = [ + FrameSignature( + scheme=HexNumber(1), + signer=Bytes(self.sender), + msg=Bytes(b""), + ) + ] + else: + # Contract senders authorize via their code; no + # protocol-validated signature is required. + self.signatures = [] + + def resolve_key(sig: FrameSignature) -> Hash | None: + if sig.secret_key is not None: + return sig.secret_key + if ( + self.sender is not None + and Bytes(self.sender) == sig.signer + and self.sender.key is not None + ): + return self.sender.key + return None + + # Explicit-digest entries first: their raw bytes are part of the + # canonical signature hash signed by empty-msg entries. + for sig in self.signatures: + if sig.scheme != 1 or len(sig.signature) > 0: + continue + if len(sig.msg) != 32: + continue + key = resolve_key(sig) + if key is not None: + sig.signed_over(bytes(sig.msg), key) + + sig_hash = self.rlp_signing_bytes().keccak256() + for sig in self.signatures: + if sig.scheme != 1 or len(sig.signature) > 0: + continue + if len(sig.msg) != 0: + continue + key = resolve_key(sig) + if key is not None: + sig.signed_over(sig_hash, key) + def sign(self: "Transaction") -> None: """Signs the authorization tuple with a private key.""" + if self.frames is not None: + self._sign_frame_signatures() + return signature_bytes: bytes | None = None rlp_signing_bytes = self.rlp_signing_bytes() if ( @@ -693,6 +866,12 @@ def with_signature_and_sender( """Return signed version of the transaction using the private key.""" updated_values: Dict[str, Any] = {} + if self.frames is not None: + # EIP-8141: The sender is explicit; fill in the signature + # entries instead of v/r/s. + self._sign_frame_signatures() + return self + if ( "v" in self.model_fields_set or "r" in self.model_fields_set @@ -762,7 +941,20 @@ def get_rlp_signing_fields(self) -> List[str]: depending on the transaction type. """ field_list: List[str] - if self.ty == 6: + if self.ty == 6 and self.frames is not None: + # EIP-8141: https://eips.ethereum.org/EIPS/eip-8141 + field_list = [ + "chain_id", + "nonce", + "sender", + "frames", + "signing_signatures", + "max_priority_fee_per_gas", + "max_fee_per_gas", + "max_fee_per_blob_gas", + "blob_versioned_hashes", + ] + elif self.ty == 6: # EIP-7873: https://eips.ethereum.org/EIPS/eip-7873 field_list = [ "chain_id", @@ -860,6 +1052,13 @@ def get_rlp_fields(self) -> List[str]: depending on the transaction type. """ fields = self.get_rlp_signing_fields() + if self.ty == 6 and self.frames is not None: + # EIP-8141: The transaction is not wrapped in a signature; + # the full encoding carries the raw signature entries. + return [ + "signatures" if field == "signing_signatures" else field + for field in fields + ] if self.ty == 0 and self.protected: fields = fields[:-3] return fields + ["v", "r", "s"] diff --git a/packages/testing/src/execution_testing/vm/opcodes.py b/packages/testing/src/execution_testing/vm/opcodes.py index cfe8c4d52b1..2b4db49efb1 100644 --- a/packages/testing/src/execution_testing/vm/opcodes.py +++ b/packages/testing/src/execution_testing/vm/opcodes.py @@ -5953,6 +5953,205 @@ class Opcodes(Opcode, Enum): Source: [evm.codes/#FF](https://www.evm.codes/#FF) """ + # EIP-8141 Frame Transaction Opcodes + + APPROVE = Opcode( + 0xAA, + popped_stack_items=3, + pushed_stack_items=0, + kwargs=["offset", "size", "scope"], + terminating=True, + ) + """ + APPROVE(offset, size, scope) + ---- + + Description + ---- + Exit the current call frame successfully like RETURN while updating + the transaction-scoped approval context of an EIP-8141 frame + transaction according to `scope` (bitmask: 1=payment, 2=execution, + 3=both). + + Inputs + ---- + - offset: byte offset in memory of the return data + - size: byte size of the return data + - scope: requested approval scope + + Outputs + ---- + None (terminates the current context) + + Fork + ---- + Bogota + + Gas: 0 (plus memory expansion) + """ + + TXPARAM = Opcode( + 0xB0, + popped_stack_items=1, + pushed_stack_items=1, + kwargs=["param"], + ) + """ + TXPARAM(param) + ---- + + Description + ---- + Push transaction-scoped information of the executing EIP-8141 frame + transaction (type, nonce, sender, fees, max cost, signature hash, + frame count, current frame index, signature count). + + Inputs + ---- + - param: parameter selector (0x00-0x0B) + + Outputs + ---- + - value: the requested transaction parameter + + Fork + ---- + Bogota + + Gas: 2 + """ + + FRAMEDATALOAD = Opcode( + 0xB1, + popped_stack_items=2, + pushed_stack_items=1, + kwargs=["offset", "frame_index"], + ) + """ + FRAMEDATALOAD(offset, frame_index) + ---- + + Description + ---- + Load one 32-byte word from the chosen frame's data with + CALLDATALOAD semantics (EIP-8141). + + Inputs + ---- + - offset: byte offset in the frame data + - frame_index: index of the frame + + Outputs + ---- + - value: 32-byte word from the frame data + + Fork + ---- + Bogota + + Gas: 3 + """ + + FRAMEDATACOPY = Opcode( + 0xB2, + popped_stack_items=4, + pushed_stack_items=0, + kwargs=["dest_offset", "offset", "size", "frame_index"], + ) + """ + FRAMEDATACOPY(dest_offset, offset, size, frame_index) + ---- + + Description + ---- + Copy the chosen frame's data into memory with CALLDATACOPY + semantics (EIP-8141). + + Inputs + ---- + - dest_offset: byte offset in memory to copy to + - offset: byte offset in the frame data to copy from + - size: number of bytes to copy + - frame_index: index of the frame + + Outputs + ---- + None + + Fork + ---- + Bogota + + Gas: 3 + 3 * ceil(size / 32) (plus memory expansion) + """ + + FRAMEPARAM = Opcode( + 0xB3, + popped_stack_items=2, + pushed_stack_items=1, + kwargs=["frame_index", "param"], + ) + """ + FRAMEPARAM(frame_index, param) + ---- + + Description + ---- + Push frame-scoped information of the chosen frame of the executing + EIP-8141 frame transaction (resolved target, gas limit, mode, + flags, data length, status, allowed approval scope, atomic batch + bit, value). + + Inputs + ---- + - frame_index: index of the frame + - param: parameter selector (0x00-0x08) + + Outputs + ---- + - value: the requested frame parameter + + Fork + ---- + Bogota + + Gas: 2 + """ + + SIGPARAM = Opcode( + 0xB4, + popped_stack_items=2, + pushed_stack_items=1, + kwargs=["signature_index", "param"], + ) + """ + SIGPARAM(signature_index, param) + ---- + + Description + ---- + Push signature-scoped metadata of the chosen signature entry of the + executing EIP-8141 frame transaction (effective signer, scheme, + msg, signature length). With `param=0x04` (arity handled manually + by the caller), copies an ARBITRARY entry's raw signature bytes to + memory with CALLDATACOPY semantics. + + Inputs + ---- + - signature_index: index of the signature entry + - param: parameter selector (0x00-0x04) + + Outputs + ---- + - value: the requested signature parameter + + Fork + ---- + Bogota + + Gas: 2 (params 0x00-0x03) + """ + _push_opcodes_byte_list: List[Opcode] = [ Opcodes.PUSH1, diff --git a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py index f9ec92d6ded..0f311763b0c 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -33,6 +33,7 @@ def tx_types(self) -> list[int]: (2, "FeeMarketTransaction"), (3, "BlobTransaction"), (4, "SetCodeTransaction"), + (6, "FrameTransaction"), ): if hasattr(transactions, attribute): tx_types.append(tx_type) @@ -292,6 +293,21 @@ def SetCodeTransaction(self) -> Any: """Set code transaction class of the fork.""" return self._module("transactions").SetCodeTransaction + @property + def FrameTransaction(self) -> Any: + """Frame transaction class of the fork.""" + return self._module("transactions").FrameTransaction + + @property + def Frame(self) -> Any: + """Frame class of the fork.""" + return self._module("transactions").Frame + + @property + def TransactionSignature(self) -> Any: + """Transaction signature class of the fork.""" + return self._module("transactions").TransactionSignature + @property def Withdrawal(self) -> Any: """Withdrawal class of the fork.""" diff --git a/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py index da882c94d99..1851c9b6d7d 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/transaction_loader.py @@ -127,6 +127,47 @@ def json_to_blob_versioned_hashes(self) -> List[Bytes32]: for blob_hash in self.raw.get("blobVersionedHashes") ] + def json_to_sender(self) -> Any: + """Get the explicit sender address of a frame transaction.""" + return self.fork.hex_to_address(self.raw.get("sender")) + + def json_to_frames(self) -> Any: + """Get the frames of a frame transaction.""" + frames = [] + for frame_data in self.raw.get("frames", []): + target_raw = frame_data.get("target") + if target_raw is None or target_raw in ("", "0x"): + target: Any = Bytes0(b"") + else: + target = self.fork.hex_to_address(target_raw) + frames.append( + self.fork.Frame( + mode=parse_hex_or_int(frame_data.get("mode", 0), Uint), + flags=parse_hex_or_int(frame_data.get("flags", 0), Uint), + target=target, + gas_limit=parse_hex_or_int( + frame_data.get("gasLimit", 0), Uint + ), + value=parse_hex_or_int(frame_data.get("value", 0), U256), + data=hex_to_bytes(frame_data.get("data", "0x")), + ) + ) + return tuple(frames) + + def json_to_signatures(self) -> Any: + """Get the signature entries of a frame transaction.""" + signatures = [] + for sig_data in self.raw.get("signatures", []): + signatures.append( + self.fork.TransactionSignature( + scheme=parse_hex_or_int(sig_data.get("scheme", 0), Uint), + signer=hex_to_bytes(sig_data.get("signer", "0x")), + msg=hex_to_bytes(sig_data.get("msg", "0x")), + signature=hex_to_bytes(sig_data.get("signature", "0x")), + ) + ) + return tuple(signatures) + def json_to_v(self) -> U256: """Get the v value of the transaction.""" return hex_to_u256( @@ -177,7 +218,12 @@ def read(self) -> Any: """Convert json transaction data to a transaction object.""" if "type" in self.raw: tx_type = parse_hex_or_int(self.raw.get("type"), Uint) - if tx_type == Uint(4): + if tx_type == Uint(6): + if not self.fork.supports_tx_type(6): + raise self.unsupported_tx_type(6) + tx_cls = self.fork.FrameTransaction + tx_byte_prefix = b"\x06" + elif tx_type == Uint(4): if not self.fork.supports_tx_type(4): raise self.unsupported_tx_type(4) tx_cls = self.fork.SetCodeTransaction diff --git a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py index 9a5a824e76b..4fc67fea3da 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -163,6 +163,15 @@ def parse_json_tx(self, raw_tx: Any) -> Any: """ t8n = self.t8n + if "frames" in raw_tx: + # EIP-8141 frame transactions carry an explicit sender and a + # signature list instead of gas/input/to/value/v/r/s. + tx = TransactionLoad(raw_tx, t8n.fork).read() + self.all_txs.append(tx) + if t8n.fork.has_decode_transaction: + return t8n.fork.decode_transaction(tx) + return tx + # for idx, json_tx in enumerate(self.data): raw_tx["gasLimit"] = raw_tx["gas"] raw_tx["data"] = raw_tx["input"] @@ -356,6 +365,46 @@ def json_encode_receipts(self) -> Any: for tx_hash, receipt in self.receipts: receipt_dict = {"transactionHash": "0x" + tx_hash.hex()} + if hasattr(receipt, "frame_receipts"): + # EIP-8141 frame transaction receipt. + receipt_dict["cumulativeGasUsed"] = hex( + receipt.cumulative_gas_used + ) + receipt_dict["payer"] = "0x" + receipt.payer.hex() + + all_logs = [] + frame_receipts_json = [] + for frame_receipt in receipt.frame_receipts: + frame_logs_json = [] + for log in frame_receipt.logs: + log_dict = { + "address": "0x" + log.address.hex(), + "topics": [ + "0x" + topic.hex() for topic in log.topics + ], + "data": "0x" + log.data.hex(), + } + frame_logs_json.append(log_dict) + all_logs.append(log_dict) + frame_receipts_json.append( + { + "status": hex(int(frame_receipt.status)), + "gasUsed": hex(int(frame_receipt.gas_used)), + "logs": frame_logs_json, + } + ) + receipt_dict["frameReceipts"] = frame_receipts_json + receipt_dict["logs"] = all_logs + + # Derive success and bloom for tooling compatibility. + receipt_dict["succeeded"] = all( + int(frame_receipt.status) == 1 + for frame_receipt in receipt.frame_receipts + ) + receipt_dict["bloom"] = "0x" + ("00" * 256) + receipts_json.append(receipt_dict) + continue + if hasattr(receipt, "succeeded"): receipt_dict["succeeded"] = receipt.succeeded else: diff --git a/vulture_whitelist.py b/vulture_whitelist.py index dc1b1e7dde9..eb4cd03fbf9 100644 --- a/vulture_whitelist.py +++ b/vulture_whitelist.py @@ -114,6 +114,9 @@ TransactionLoad.json_to_nonce TransactionLoad.json_to_gas TransactionLoad.json_to_to +TransactionLoad.json_to_sender +TransactionLoad.json_to_frames +TransactionLoad.json_to_signatures TransactionLoad.json_to_value TransactionLoad.json_to_data TransactionLoad.json_to_access_list @@ -122,6 +125,9 @@ TransactionLoad.json_to_max_priority_fee_per_gas TransactionLoad.json_to_max_fee_per_blob_gas TransactionLoad.json_to_blob_versioned_hashes +TransactionLoad.json_to_sender +TransactionLoad.json_to_frames +TransactionLoad.json_to_signatures TransactionLoad.json_to_v TransactionLoad.json_to_y_parity TransactionLoad.json_to_r From 6049b1db80816b562c23ad759c17cd3f257172d8 Mon Sep 17 00:00:00 2001 From: lightclient Date: Mon, 6 Jul 2026 07:09:30 -0600 Subject: [PATCH 5/9] feat(tests): add end-to-end tests for frame transactions Broad-stroke coverage of EIP-8141: default-code transfer, contract sender approving via APPROVE, EOA paymaster, atomic batch rollback, TXPARAM introspection, and invalid transactions (SENDER frame before approval, reverting VERIFY frame). --- .../unscheduled/eip8141_frame_tx/__init__.py | 1 + tests/unscheduled/eip8141_frame_tx/helpers.py | 19 + tests/unscheduled/eip8141_frame_tx/spec.py | 81 +++++ .../test_frame_transactions.py | 342 ++++++++++++++++++ 4 files changed, 443 insertions(+) create mode 100644 tests/unscheduled/eip8141_frame_tx/__init__.py create mode 100644 tests/unscheduled/eip8141_frame_tx/helpers.py create mode 100644 tests/unscheduled/eip8141_frame_tx/spec.py create mode 100644 tests/unscheduled/eip8141_frame_tx/test_frame_transactions.py diff --git a/tests/unscheduled/eip8141_frame_tx/__init__.py b/tests/unscheduled/eip8141_frame_tx/__init__.py new file mode 100644 index 00000000000..5a44c078a7a --- /dev/null +++ b/tests/unscheduled/eip8141_frame_tx/__init__.py @@ -0,0 +1 @@ +"""Tests for EIP-8141 frame transactions.""" diff --git a/tests/unscheduled/eip8141_frame_tx/helpers.py b/tests/unscheduled/eip8141_frame_tx/helpers.py new file mode 100644 index 00000000000..e71d651192e --- /dev/null +++ b/tests/unscheduled/eip8141_frame_tx/helpers.py @@ -0,0 +1,19 @@ +"""Helpers for EIP-8141 frame transaction tests.""" + +from execution_testing import Bytecode, Op + +from .spec import Spec + + +def approve_bytecode( + scope: int = Spec.APPROVE_EXECUTION_AND_PAYMENT, +) -> Bytecode: + """ + Return bytecode that calls `APPROVE` with the given scope and no + return data. + + `APPROVE` succeeds only when the executing account is the frame's + resolved target, so this code is meant to be deployed at the account + a `VERIFY` frame targets. + """ + return Op.APPROVE(0, 0, scope) diff --git a/tests/unscheduled/eip8141_frame_tx/spec.py b/tests/unscheduled/eip8141_frame_tx/spec.py new file mode 100644 index 00000000000..8d868883e59 --- /dev/null +++ b/tests/unscheduled/eip8141_frame_tx/spec.py @@ -0,0 +1,81 @@ +"""Defines EIP-8141 specification constants and types.""" + +from dataclasses import dataclass + +from execution_testing import Address + + +@dataclass(frozen=True) +class ReferenceSpec: + """Defines the reference spec version and git path.""" + + git_path: str + version: str + + +ref_spec_8141 = ReferenceSpec( + "EIPS/eip-8141.md", "b18941a2e3966e7f47c6faa8b51d3afef9d3bcc3" +) + + +@dataclass(frozen=True) +class Spec: + """ + Parameters from the EIP-8141 specification as defined at + https://eips.ethereum.org/EIPS/eip-8141. + """ + + FRAME_TX_TYPE = 0x06 + FRAME_TX_INTRINSIC_COST = 15_000 + FRAME_TX_PER_FRAME_COST = 475 + ENTRY_POINT = Address(0xAA) + EXPIRY_VERIFIER = Address(0x8141) + EXPIRY_DATA_LENGTH = 8 + MAX_FRAMES = 64 + + # Frame modes + MODE_DEFAULT = 0 + MODE_VERIFY = 1 + MODE_SENDER = 2 + + # Frame flags + APPROVE_NONE = 0x0 + APPROVE_PAYMENT = 0x1 + APPROVE_EXECUTION = 0x2 + APPROVE_EXECUTION_AND_PAYMENT = 0x3 + ATOMIC_BATCH_FLAG = 0x4 + + # Signature schemes + SCHEME_ARBITRARY = 0x0 + SCHEME_SECP256K1 = 0x1 + SCHEME_P256 = 0x2 + + # Frame receipt statuses + STATUS_FAILURE = 0 + STATUS_SUCCESS = 1 + STATUS_SKIPPED = 3 + + # TXPARAM selectors + TXPARAM_TYPE = 0x00 + TXPARAM_NONCE = 0x01 + TXPARAM_SENDER = 0x02 + TXPARAM_MAX_PRIORITY_FEE = 0x03 + TXPARAM_MAX_FEE = 0x04 + TXPARAM_MAX_BLOB_FEE = 0x05 + TXPARAM_MAX_COST = 0x06 + TXPARAM_BLOB_COUNT = 0x07 + TXPARAM_SIG_HASH = 0x08 + TXPARAM_FRAME_COUNT = 0x09 + TXPARAM_FRAME_INDEX = 0x0A + TXPARAM_SIGNATURE_COUNT = 0x0B + + # FRAMEPARAM selectors + FRAMEPARAM_TARGET = 0x00 + FRAMEPARAM_GAS_LIMIT = 0x01 + FRAMEPARAM_MODE = 0x02 + FRAMEPARAM_FLAGS = 0x03 + FRAMEPARAM_DATA_LENGTH = 0x04 + FRAMEPARAM_STATUS = 0x05 + FRAMEPARAM_ALLOWED_SCOPE = 0x06 + FRAMEPARAM_ATOMIC_BATCH = 0x07 + FRAMEPARAM_VALUE = 0x08 diff --git a/tests/unscheduled/eip8141_frame_tx/test_frame_transactions.py b/tests/unscheduled/eip8141_frame_tx/test_frame_transactions.py new file mode 100644 index 00000000000..8b8c7926503 --- /dev/null +++ b/tests/unscheduled/eip8141_frame_tx/test_frame_transactions.py @@ -0,0 +1,342 @@ +""" +Broad-stroke end-to-end tests for +[EIP-8141: Frame Transaction](https://eips.ethereum.org/EIPS/eip-8141). + +These tests cover the core flows of the frame transaction: default-code +validation and payment, contract senders approving via `APPROVE`, +third-party payers, atomic batches, transaction introspection, and the +basic invalid-transaction cases. Exhaustive edge-case coverage is left +for follow-up work. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Bytes, + Environment, + Frame, + FrameSignature, + Op, + StateTestFiller, + Transaction, + TransactionException, +) + +from .helpers import approve_bytecode +from .spec import Spec, ref_spec_8141 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8141.git_path +REFERENCE_SPEC_VERSION = ref_spec_8141.version + +pytestmark = pytest.mark.valid_from("Bogota") + +SLOT_EXECUTED = 0x01 +"""Storage slot used by target contracts to record execution.""" + + +def test_transfer_with_default_code( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + Transfer ETH from an EOA sender using the default code: a `VERIFY` + frame authorizes execution and payment against the sender's + signature entry, and a `SENDER` frame carries the value. + """ + sender = pre.fund_eoa() + recipient = pre.fund_eoa(amount=1) + transfer_value = 10**17 + + tx = Transaction( + sender=sender, + frames=[ + Frame( + mode=Spec.MODE_VERIFY, + flags=Spec.APPROVE_EXECUTION_AND_PAYMENT, + gas_limit=100_000, + ), + Frame( + mode=Spec.MODE_SENDER, + target=recipient, + gas_limit=100_000, + value=transfer_value, + ), + ], + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={ + sender: Account(nonce=1), + recipient: Account(balance=1 + transfer_value), + }, + ) + + +def test_contract_sender_approves( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + Send a frame transaction from a contract account whose code calls + `APPROVE` to authorize execution and payment, then executes a + `SENDER` frame calling another contract. + """ + sender = pre.deploy_contract( + code=approve_bytecode(Spec.APPROVE_EXECUTION_AND_PAYMENT), + balance=10**18, + ) + target = pre.deploy_contract(code=Op.SSTORE(SLOT_EXECUTED, 1) + Op.STOP) + + tx = Transaction( + sender=sender, + nonce=1, + frames=[ + Frame( + mode=Spec.MODE_VERIFY, + flags=Spec.APPROVE_EXECUTION_AND_PAYMENT, + gas_limit=100_000, + ), + Frame( + mode=Spec.MODE_SENDER, + target=target, + gas_limit=200_000, + ), + ], + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={ + sender: Account(nonce=2), + target: Account(storage={SLOT_EXECUTED: 1}), + }, + ) + + +def test_eoa_paymaster( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + Sponsor a frame transaction's fees from a second EOA via the + default code: the sender approves only execution, the payer + approves only payment, and the sender's balance is untouched. + """ + sender_balance = 10**18 + sender = pre.fund_eoa(amount=sender_balance) + payer = pre.fund_eoa() + target = pre.deploy_contract(code=Op.SSTORE(SLOT_EXECUTED, 1) + Op.STOP) + + tx = Transaction( + sender=sender, + frames=[ + Frame( + mode=Spec.MODE_VERIFY, + flags=Spec.APPROVE_EXECUTION, + gas_limit=100_000, + ), + Frame( + mode=Spec.MODE_VERIFY, + flags=Spec.APPROVE_PAYMENT, + target=payer, + gas_limit=100_000, + ), + Frame( + mode=Spec.MODE_SENDER, + target=target, + gas_limit=200_000, + ), + ], + signatures=[ + FrameSignature( + scheme=Spec.SCHEME_SECP256K1, + signer=Bytes(sender), + ), + FrameSignature( + scheme=Spec.SCHEME_SECP256K1, + signer=Bytes(payer), + secret_key=payer.key, + ), + ], + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={ + sender: Account(nonce=1, balance=sender_balance), + payer: Account(nonce=0), + target: Account(storage={SLOT_EXECUTED: 1}), + }, + ) + + +def test_atomic_batch_rollback( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + Roll back an atomic batch: a `SENDER` frame with the atomic batch + flag writes storage, and the subsequent frame terminating the batch + reverts, discarding the write. + """ + sender = pre.fund_eoa() + target = pre.deploy_contract(code=Op.SSTORE(SLOT_EXECUTED, 1) + Op.STOP) + reverter = pre.deploy_contract(code=Op.REVERT(0, 0)) + + tx = Transaction( + sender=sender, + frames=[ + Frame( + mode=Spec.MODE_VERIFY, + flags=Spec.APPROVE_EXECUTION_AND_PAYMENT, + gas_limit=100_000, + ), + Frame( + mode=Spec.MODE_SENDER, + flags=Spec.ATOMIC_BATCH_FLAG, + target=target, + gas_limit=200_000, + ), + Frame( + mode=Spec.MODE_SENDER, + target=reverter, + gas_limit=100_000, + ), + ], + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={ + sender: Account(nonce=1), + target: Account(storage={SLOT_EXECUTED: 0}), + }, + ) + + +def test_txparam_sender_introspection( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + Read the transaction sender through `TXPARAM` from a `DEFAULT` + frame and store it. + """ + sender = pre.fund_eoa() + target = pre.deploy_contract( + code=Op.SSTORE(SLOT_EXECUTED, Op.TXPARAM(Spec.TXPARAM_SENDER)) + + Op.STOP + ) + + tx = Transaction( + sender=sender, + frames=[ + Frame( + mode=Spec.MODE_VERIFY, + flags=Spec.APPROVE_EXECUTION_AND_PAYMENT, + gas_limit=100_000, + ), + Frame( + mode=Spec.MODE_DEFAULT, + target=target, + gas_limit=200_000, + ), + ], + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={ + target: Account(storage={SLOT_EXECUTED: sender}), + }, + ) + + +@pytest.mark.exception_test +def test_sender_frame_before_approval( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + Reject a frame transaction whose `SENDER` frame runs before any + frame has approved execution. + """ + sender = pre.fund_eoa() + target = pre.deploy_contract(code=Op.SSTORE(SLOT_EXECUTED, 1) + Op.STOP) + + tx = Transaction( + sender=sender, + frames=[ + Frame( + mode=Spec.MODE_SENDER, + target=target, + gas_limit=200_000, + ), + Frame( + mode=Spec.MODE_VERIFY, + flags=Spec.APPROVE_EXECUTION_AND_PAYMENT, + gas_limit=100_000, + ), + ], + error=TransactionException.TYPE_6_INVALID_FRAME_EXECUTION, + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={ + target: Account(storage={SLOT_EXECUTED: 0}), + }, + ) + + +@pytest.mark.exception_test +def test_verify_frame_reverts( + state_test: StateTestFiller, + pre: Alloc, +) -> None: + """ + Reject a frame transaction with a reverting `VERIFY` frame: the + sender contract allows no approval scope, so the default code + reverts. + """ + sender = pre.fund_eoa() + reverter = pre.deploy_contract(code=Op.REVERT(0, 0)) + + tx = Transaction( + sender=sender, + frames=[ + Frame( + mode=Spec.MODE_VERIFY, + flags=Spec.APPROVE_EXECUTION_AND_PAYMENT, + gas_limit=100_000, + ), + Frame( + mode=Spec.MODE_VERIFY, + flags=Spec.APPROVE_NONE, + target=reverter, + gas_limit=100_000, + ), + ], + error=TransactionException.TYPE_6_INVALID_FRAME_EXECUTION, + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={}, + ) From 8a8218ee6cb4abf90145493183c432b34dd6b108 Mon Sep 17 00:00:00 2001 From: lightclient Date: Mon, 6 Jul 2026 07:37:37 -0600 Subject: [PATCH 6/9] feat(testing): verify frame transaction receipts per frame Adds verify_frame_transaction_receipt, run only for frame transactions on top of the generic receipt checks: it validates the receipt payer and the per-frame receipt entries (status, gas used, and logs) against expected_receipt. Non-frame uses of expected_receipt are unaffected. The frame transaction tests now assert the payer and per-frame statuses, including the unrolled (0x0) and skipped (0x3) statuses of both atomic batch failure orderings. --- .../src/execution_testing/specs/helpers.py | 98 +++++++++++++++++++ .../test_frame_transactions.py | 91 ++++++++++++++--- 2 files changed, 175 insertions(+), 14 deletions(-) diff --git a/packages/testing/src/execution_testing/specs/helpers.py b/packages/testing/src/execution_testing/specs/helpers.py index a176cd1c59b..83800e8f6f3 100644 --- a/packages/testing/src/execution_testing/specs/helpers.py +++ b/packages/testing/src/execution_testing/specs/helpers.py @@ -340,6 +340,100 @@ def verify_transaction_receipt( # TODO: Add more fields as needed +def verify_frame_transaction_receipt( + transaction_index: int, + expected_receipt: TransactionReceipt | None, + actual_receipt: TransactionReceipt | None, +) -> None: + """ + Verify the frame-transaction-specific fields of the actual receipt + against the expected one: the `payer` and the per-frame receipt + entries defined by [EIP-8141]. + + Only called for frame transactions, on top of the generic + [`verify_transaction_receipt`][vtr] checks. If the expected receipt + is None, validation is skipped. Only non-None values in the + expected receipt are verified; within an expected frame receipt + entry, only its non-None fields are verified. + + [vtr]: ref:execution_testing.specs.helpers.verify_transaction_receipt + [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 + """ + if expected_receipt is None: + return + assert actual_receipt is not None + if ( + expected_receipt.payer is not None + and actual_receipt.payer != expected_receipt.payer + ): + raise TransactionReceiptMismatchError( + index=transaction_index, + field_name="payer", + expected_value=expected_receipt.payer, + actual_value=actual_receipt.payer, + ) + + if expected_receipt.frame_receipts is None: + return + actual_frame_receipts = actual_receipt.frame_receipts + if actual_frame_receipts is None: + raise TransactionReceiptMismatchError( + index=transaction_index, + field_name="frame_receipts", + expected_value=expected_receipt.frame_receipts, + actual_value=None, + ) + if len(expected_receipt.frame_receipts) != len(actual_frame_receipts): + raise TransactionReceiptMismatchError( + index=transaction_index, + field_name="frame_receipt_count", + expected_value=len(expected_receipt.frame_receipts), + actual_value=len(actual_frame_receipts), + ) + for frame_idx, (expected_frame, actual_frame) in enumerate( + zip( + expected_receipt.frame_receipts, + actual_frame_receipts, + strict=True, + ) + ): + if ( + expected_frame.status is not None + and actual_frame.status != expected_frame.status + ): + raise TransactionReceiptMismatchError( + index=transaction_index, + field_name=f"frame_receipts[{frame_idx}].status", + expected_value=expected_frame.status, + actual_value=actual_frame.status, + ) + if ( + expected_frame.gas_used is not None + and actual_frame.gas_used != expected_frame.gas_used + ): + raise TransactionReceiptMismatchError( + index=transaction_index, + field_name=f"frame_receipts[{frame_idx}].gas_used", + expected_value=expected_frame.gas_used, + actual_value=actual_frame.gas_used, + ) + if expected_frame.logs is not None: + actual_frame_logs = actual_frame.logs or [] + if len(expected_frame.logs) != len(actual_frame_logs): + raise TransactionReceiptMismatchError( + index=transaction_index, + field_name=f"frame_receipts[{frame_idx}].log_count", + expected_value=len(expected_frame.logs), + actual_value=len(actual_frame_logs), + ) + for log_idx, (expected_log, actual_log) in enumerate( + zip(expected_frame.logs, actual_frame_logs, strict=True) + ): + verify_log( + transaction_index, log_idx, expected_log, actual_log + ) + + def verify_transactions( *, txs: List[Transaction], @@ -369,6 +463,10 @@ def verify_transactions( verify_transaction_receipt( i, tx.expected_receipt, result.receipts[receipt_index] ) + if tx.frames is not None: + verify_frame_transaction_receipt( + i, tx.expected_receipt, result.receipts[receipt_index] + ) receipt_index += 1 return list(rejected_txs.keys()) diff --git a/tests/unscheduled/eip8141_frame_tx/test_frame_transactions.py b/tests/unscheduled/eip8141_frame_tx/test_frame_transactions.py index 8b8c7926503..c57a59c6436 100644 --- a/tests/unscheduled/eip8141_frame_tx/test_frame_transactions.py +++ b/tests/unscheduled/eip8141_frame_tx/test_frame_transactions.py @@ -16,11 +16,13 @@ Bytes, Environment, Frame, + FrameReceipt, FrameSignature, Op, StateTestFiller, Transaction, TransactionException, + TransactionReceipt, ) from .helpers import approve_bytecode @@ -63,6 +65,13 @@ def test_transfer_with_default_code( value=transfer_value, ), ], + expected_receipt=TransactionReceipt( + payer=sender, + frame_receipts=[ + FrameReceipt(status=Spec.STATUS_SUCCESS, logs=[]), + FrameReceipt(status=Spec.STATUS_SUCCESS), + ], + ), ) state_test( @@ -106,6 +115,13 @@ def test_contract_sender_approves( gas_limit=200_000, ), ], + expected_receipt=TransactionReceipt( + payer=sender, + frame_receipts=[ + FrameReceipt(status=Spec.STATUS_SUCCESS), + FrameReceipt(status=Spec.STATUS_SUCCESS), + ], + ), ) state_test( @@ -164,6 +180,14 @@ def test_eoa_paymaster( secret_key=payer.key, ), ], + expected_receipt=TransactionReceipt( + payer=payer, + frame_receipts=[ + FrameReceipt(status=Spec.STATUS_SUCCESS), + FrameReceipt(status=Spec.STATUS_SUCCESS), + FrameReceipt(status=Spec.STATUS_SUCCESS), + ], + ), ) state_test( @@ -178,19 +202,64 @@ def test_eoa_paymaster( ) +@pytest.mark.parametrize( + "revert_position", + [ + pytest.param("last", id="unrolls_executed_frames"), + pytest.param("first", id="skips_remaining_frames"), + ], +) def test_atomic_batch_rollback( state_test: StateTestFiller, pre: Alloc, + revert_position: str, ) -> None: """ - Roll back an atomic batch: a `SENDER` frame with the atomic batch - flag writes storage, and the subsequent frame terminating the batch - reverts, discarding the write. + Roll back an atomic batch containing a reverting frame. + + When the batch terminator reverts, the storage write of the + already executed batch frame is unrolled and both frames report + failure. When the first batch frame reverts, the remaining batch + frame is skipped with status `0x3` and no gas consumed. In both + cases the storage write is discarded. """ sender = pre.fund_eoa() target = pre.deploy_contract(code=Op.SSTORE(SLOT_EXECUTED, 1) + Op.STOP) reverter = pre.deploy_contract(code=Op.REVERT(0, 0)) + store_frame_flags = ( + Spec.ATOMIC_BATCH_FLAG if revert_position == "last" else 0 + ) + revert_frame_flags = ( + Spec.ATOMIC_BATCH_FLAG if revert_position == "first" else 0 + ) + store_frame = Frame( + mode=Spec.MODE_SENDER, + flags=store_frame_flags, + target=target, + gas_limit=200_000, + ) + revert_frame = Frame( + mode=Spec.MODE_SENDER, + flags=revert_frame_flags, + target=reverter, + gas_limit=100_000, + ) + if revert_position == "last": + batch = [store_frame, revert_frame] + expected_frame_receipts = [ + FrameReceipt(status=Spec.STATUS_SUCCESS), + FrameReceipt(status=Spec.STATUS_FAILURE), + FrameReceipt(status=Spec.STATUS_FAILURE), + ] + else: + batch = [revert_frame, store_frame] + expected_frame_receipts = [ + FrameReceipt(status=Spec.STATUS_SUCCESS), + FrameReceipt(status=Spec.STATUS_FAILURE), + FrameReceipt(status=Spec.STATUS_SKIPPED, gas_used=0), + ] + tx = Transaction( sender=sender, frames=[ @@ -199,18 +268,12 @@ def test_atomic_batch_rollback( flags=Spec.APPROVE_EXECUTION_AND_PAYMENT, gas_limit=100_000, ), - Frame( - mode=Spec.MODE_SENDER, - flags=Spec.ATOMIC_BATCH_FLAG, - target=target, - gas_limit=200_000, - ), - Frame( - mode=Spec.MODE_SENDER, - target=reverter, - gas_limit=100_000, - ), + *batch, ], + expected_receipt=TransactionReceipt( + payer=sender, + frame_receipts=expected_frame_receipts, + ), ) state_test( From e6cf8766bec4dd808aec643112eb523ff5e2fe4d Mon Sep 17 00:00:00 2001 From: lightclient Date: Mon, 6 Jul 2026 08:41:12 -0600 Subject: [PATCH 7/9] eip-8141: align implementation details with cross-client testing Validating the geth implementation against the EELS-produced fixtures surfaced three differences: - do not read the frame caller's balance for zero-value frames; the gratuitous read placed the entry point in the block access list - report frame transactions as succeeded in the t8n receipt output: transactions that make it into a block are valid regardless of individual frame results - derive the t8n receipt logs bloom from the frame logs instead of emitting zeroes Also maps the type-6 transaction exceptions for the geth t8n. --- .../src/execution_testing/client_clis/clis/geth.py | 9 +++++++++ src/ethereum/forks/bogota/fork.py | 11 ++++++----- src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py | 14 +++++++++----- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/testing/src/execution_testing/client_clis/clis/geth.py b/packages/testing/src/execution_testing/client_clis/clis/geth.py index 1a9c46c720d..909df99d20b 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/geth.py +++ b/packages/testing/src/execution_testing/client_clis/clis/geth.py @@ -94,6 +94,15 @@ class GethExceptionMapper(ExceptionMapper): TransactionException.TYPE_4_TX_PRE_FORK: ( "transaction type not supported" ), + TransactionException.TYPE_6_INVALID_FRAME_FORMAT: ( + "invalid frame tx format" + ), + TransactionException.TYPE_6_INVALID_SIGNATURE: ( + "invalid frame tx signature" + ), + TransactionException.TYPE_6_INVALID_FRAME_EXECUTION: ( + "invalid frame execution" + ), TransactionException.INITCODE_SIZE_EXCEEDED: ( "max initcode size exceeded" ), diff --git a/src/ethereum/forks/bogota/fork.py b/src/ethereum/forks/bogota/fork.py index 93ffd8981ad..800c80f4f20 100644 --- a/src/ethereum/forks/bogota/fork.py +++ b/src/ethereum/forks/bogota/fork.py @@ -1496,11 +1496,12 @@ def execute_frame( # As with an ordinary `CALL`, a frame whose caller cannot cover the # transferred value reverts without executing. - caller_balance = get_account(tx_state, frame_caller).balance - if U256(caller_balance) < frame.value: - return default_code_output( - frame, error=Revert("insufficient balance for frame value") - ) + if frame.value != 0: + caller_balance = get_account(tx_state, frame_caller).balance + if U256(caller_balance) < frame.value: + return default_code_output( + frame, error=Revert("insufficient balance for frame value") + ) frame_accessed_addresses = set(accessed_addresses) frame_accessed_addresses.add(resolved_target) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py index 4fc67fea3da..208062ff4f2 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -337,6 +337,7 @@ def update(self, t8n: "T8N", block_env: Any, block_output: Any) -> None: # Apply diffs to pre-state for alloc output apply_changes_to_state(t8n.alloc.state, block_diff) self.receipts = self.get_receipts_from_output(t8n, block_output) + self._logs_bloom = t8n.fork.logs_bloom if hasattr(block_env, "base_fee_per_gas"): self.base_fee = block_env.base_fee_per_gas @@ -397,11 +398,14 @@ def json_encode_receipts(self) -> Any: receipt_dict["logs"] = all_logs # Derive success and bloom for tooling compatibility. - receipt_dict["succeeded"] = all( - int(frame_receipt.status) == 1 - for frame_receipt in receipt.frame_receipts - ) - receipt_dict["bloom"] = "0x" + ("00" * 256) + # Frame transactions that make it into a block are valid, + # regardless of individual frame results. + receipt_dict["succeeded"] = True + frame_logs = [] + for frame_receipt in receipt.frame_receipts: + frame_logs.extend(frame_receipt.logs) + bloom = self._logs_bloom(tuple(frame_logs)) + receipt_dict["bloom"] = "0x" + bloom.hex() receipts_json.append(receipt_dict) continue From cd540d2a595b629ed23ee51d8990842298193d66 Mon Sep 17 00:00:00 2001 From: lightclient Date: Fri, 17 Jul 2026 04:42:38 -0600 Subject: [PATCH 8/9] frames: proper integration into Transaction object --- src/ethereum/forks/bogota/fork.py | 28 ++-- src/ethereum/forks/bogota/transactions.py | 176 ++++++++++++--------- src/ethereum/forks/bogota/utils/message.py | 8 +- 3 files changed, 121 insertions(+), 91 deletions(-) diff --git a/src/ethereum/forks/bogota/fork.py b/src/ethereum/forks/bogota/fork.py index 800c80f4f20..8320611fbfe 100644 --- a/src/ethereum/forks/bogota/fork.py +++ b/src/ethereum/forks/bogota/fork.py @@ -114,9 +114,9 @@ FrameTransaction, LegacyTransaction, SetCodeTransaction, - StandardTransaction, Transaction, calculate_frame_transaction_calldata_floor, + calculate_frame_transaction_gas_limit, chain_id, compute_frame_signature_hash, decode_transaction, @@ -126,7 +126,6 @@ recover_sender, resolve_frame_target, validate_frame_signature, - validate_frame_transaction, validate_transaction, ) from .utils.hexadecimal import hex_to_address @@ -542,13 +541,18 @@ def validate_header( def check_transaction( block_env: vm.BlockEnvironment, block_output: vm.BlockOutput, - tx: StandardTransaction, + tx: Transaction, sender: Address, tx_state: TransactionState, ) -> Tuple[Uint, Tuple[VersionedHash, ...], U64]: """ Check if the transaction is includable in the block. + Frame transactions are checked by [`check_frame_transaction`][cft] + instead and never reach this function. + + [cft]: ref:ethereum.forks.bogota.fork.check_frame_transaction + Parameters ---------- block_env : @@ -606,6 +610,7 @@ def check_transaction( is empty. """ + assert not isinstance(tx, FrameTransaction) regular_gas_available = ( block_env.block_gas_limit - block_output.block_gas_used ) @@ -1064,10 +1069,6 @@ def process_transaction( encode_transaction(tx), ) - if isinstance(tx, FrameTransaction): - process_frame_transaction(block_env, block_output, tx, index, tx_state) - return - tx_chain_id = chain_id(tx) if tx_chain_id is not None and tx_chain_id != block_env.chain_id: raise WrongChainIdError( @@ -1075,6 +1076,10 @@ def process_transaction( actual=tx_chain_id, ) + if isinstance(tx, FrameTransaction): + process_frame_transaction(block_env, block_output, tx, index, tx_state) + return + sender = recover_sender(tx) intrinsic = validate_transaction(tx, sender) @@ -1264,12 +1269,6 @@ def check_frame_transaction( [EIP-3607]: https://eips.ethereum.org/EIPS/eip-3607 """ - if tx.chain_id != U256(block_env.chain_id): - raise WrongChainIdError( - expected=block_env.chain_id, - actual=tx.chain_id, - ) - regular_gas_available = ( block_env.block_gas_limit - block_output.block_gas_used ) @@ -1571,7 +1570,8 @@ def process_frame_transaction( The transaction state tracker. """ - tx_gas_limit = validate_frame_transaction(tx) + validate_transaction(tx, tx.sender) + tx_gas_limit = calculate_frame_transaction_gas_limit(tx) effective_gas_price, tx_blob_gas_used = check_frame_transaction( block_env=block_env, diff --git a/src/ethereum/forks/bogota/transactions.py b/src/ethereum/forks/bogota/transactions.py index 3fbf63e0d86..92d6c8f002f 100644 --- a/src/ethereum/forks/bogota/transactions.py +++ b/src/ethereum/forks/bogota/transactions.py @@ -763,7 +763,7 @@ class FrameTransaction: [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 """ - chain_id: U256 + chain_id: U64 """ The ID of the chain on which this transaction is executed. """ @@ -813,23 +813,15 @@ class FrameTransaction: """ -StandardTransaction = ( +Transaction = ( LegacyTransaction | AccessListTransaction | FeeMarketTransaction | BlobTransaction | SetCodeTransaction + | FrameTransaction ) """ -Union type representing transaction types authenticated by a single -ECDSA signature, i.e. every type except [`FrameTransaction`]. - -[`FrameTransaction`]: ref:ethereum.forks.bogota.transactions.FrameTransaction -""" - - -Transaction = StandardTransaction | FrameTransaction -""" Union type representing any valid transaction type. """ @@ -920,9 +912,7 @@ def decode_transaction(tx: LegacyTransaction | Bytes) -> Transaction: return tx -def validate_transaction( - tx: StandardTransaction, sender: Address -) -> IntrinsicGasCost: +def validate_transaction(tx: Transaction, sender: Address) -> IntrinsicGasCost: """ Verifies a transaction. @@ -940,6 +930,12 @@ def validate_transaction( Also, the code size of a contract creation transaction must be within limits of the protocol. + Frame transactions have no gas limit field: their gas limit is derived + from the transaction contents, so the intrinsic cost is covered by + construction. Their structure is checked by + [`validate_frame_transaction`][vft] and their calldata floor is + validated against the derived gas limit. + This function takes a transaction and gas_limit as parameters and returns the intrinsic gas costs for the transaction after validation. It throws an `InsufficientTransactionGasError` exception if the @@ -951,25 +947,37 @@ def validate_transaction( [EIP-2681]: https://eips.ethereum.org/EIPS/eip-2681 [EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 + [vft]: ref:ethereum.forks.bogota.transactions.validate_frame_transaction """ from .vm.interpreter import MAX_INIT_CODE_SIZE intrinsic = calculate_intrinsic_cost(tx, sender) - intrinsic_gas = Uint(intrinsic.regular) + Uint(intrinsic.state) - if intrinsic_gas > tx.gas: - raise InsufficientTransactionGasError("Insufficient intrinsic gas") - if intrinsic.calldata_floor > tx.gas: - raise InsufficientTransactionGasError("Insufficient calldata floor") - if tx.to == Bytes0(b"") and len(tx.data) > MAX_INIT_CODE_SIZE: - raise InitCodeTooLargeError("Code size too large") - if intrinsic.regular > TX_MAX_GAS_LIMIT: - raise InsufficientTransactionGasError( - "Intrinsic regular gas exceeds TX_MAX_GAS_LIMIT" - ) - if intrinsic.calldata_floor > TX_MAX_GAS_LIMIT: - raise InsufficientTransactionGasError( - "Intrinsic calldata floor exceeds TX_MAX_GAS_LIMIT" - ) + if isinstance(tx, FrameTransaction): + validate_frame_transaction(tx) + if intrinsic.calldata_floor > calculate_frame_transaction_gas_limit( + tx + ): + raise InsufficientTransactionGasError( + "Insufficient calldata floor" + ) + else: + intrinsic_gas = Uint(intrinsic.regular) + Uint(intrinsic.state) + if intrinsic_gas > tx.gas: + raise InsufficientTransactionGasError("Insufficient intrinsic gas") + if intrinsic.calldata_floor > tx.gas: + raise InsufficientTransactionGasError( + "Insufficient calldata floor" + ) + if tx.to == Bytes0(b"") and len(tx.data) > MAX_INIT_CODE_SIZE: + raise InitCodeTooLargeError("Code size too large") + if intrinsic.regular > TX_MAX_GAS_LIMIT: + raise InsufficientTransactionGasError( + "Intrinsic regular gas exceeds TX_MAX_GAS_LIMIT" + ) + if intrinsic.calldata_floor > TX_MAX_GAS_LIMIT: + raise InsufficientTransactionGasError( + "Intrinsic calldata floor exceeds TX_MAX_GAS_LIMIT" + ) if U256(tx.nonce) >= U256(U64.MAX_VALUE): raise NonceOverflowError("Nonce too high") @@ -977,7 +985,7 @@ def validate_transaction( def calculate_intrinsic_cost( - tx: StandardTransaction, sender: Address + tx: Transaction, sender: Address ) -> IntrinsicGasCost: """ Calculates the gas that is charged before execution is started. @@ -1005,6 +1013,10 @@ def calculate_intrinsic_cost( Self-transfers (``sender == tx.to``) skip the recipient and value charges. + The intrinsic cost of a frame transaction is instead the base cost, the + per-frame cost, the calldata cost of the frame and signature byte + fields, and the signature verification cost. + This function takes a transaction and gas_limit as parameters and returns the intrinsic regular gas cost, intrinsic state gas cost, and the minimum gas cost used by the transaction based on the calldata size. @@ -1015,6 +1027,29 @@ def calculate_intrinsic_cost( init_code_cost, ) + if isinstance(tx, FrameTransaction): + signature_gas = Uint(0) + for sig in tx.signatures: + signature_gas += signature_verification_gas(sig) + + calldata_tokens = Uint(0) + for charged_data in frame_transaction_charged_data(tx): + calldata_tokens += count_tokens_in_data(charged_data) + + regular_gas = ( + FRAME_TX_INTRINSIC_COST + + ulen(tx.frames) * FRAME_TX_PER_FRAME_COST + + calldata_tokens * GasCosts.TX_DATA_TOKEN_STANDARD + + signature_gas + ) + return IntrinsicGasCost( + regular=RegularGas(regular_gas), + state=StateGas(Uint(0)), + calldata_floor=RegularGas( + calculate_frame_transaction_calldata_floor(tx) + ), + ) + tokens_in_calldata = count_tokens_in_data(tx.data) data_cost = tokens_in_calldata * GasCosts.TX_DATA_TOKEN_STANDARD @@ -1104,7 +1139,7 @@ def count_tokens_in_data(data: bytes) -> Uint: return num_zeros + num_non_zeros * Uint(4) -def chain_id(tx: StandardTransaction) -> None | U64: +def chain_id(tx: Transaction) -> None | U64: """ Extract the chain identifier from a transaction. See [EIP-155]. @@ -1122,7 +1157,7 @@ def chain_id(tx: StandardTransaction) -> None | U64: return tx.chain_id -def recover_sender(tx: StandardTransaction) -> Address: +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -1132,10 +1167,15 @@ def recover_sender(tx: StandardTransaction) -> Address: signing hash of the transaction. The sender's public key can be obtained with these two values and therefore the sender address can be retrieved. + Frame transactions declare their sender explicitly and are + authenticated by their signature list, so they never reach this + function. + This function takes chain_id and a transaction as parameters and returns the address of the sender of the transaction. It raises an `InvalidSignatureError` if the signature values (r, s, v) are invalid. """ + assert not isinstance(tx, FrameTransaction) r, s = tx.r, tx.s if U256(0) >= r or r >= SECP256K1N: raise InvalidSignatureError("bad r") @@ -1397,18 +1437,14 @@ def is_expiry_verifier_frame(frame: Frame) -> bool: return frame.mode == FRAME_MODE_VERIFY and frame.target == EXPIRY_VERIFIER -def validate_frame_transaction(tx: FrameTransaction) -> Uint: +def validate_frame_transaction(tx: FrameTransaction) -> None: """ - Verify the static constraints of a frame transaction and return its - total gas limit. + Verify the static constraints of a frame transaction. The frame count, frame fields, and signature entry structure are checked against the limits defined in [EIP-8141]. A - `FrameTransactionFormatError` is raised for any violation and a - `NonceOverflowError` is raised when the nonce exceeds the [EIP-2681] - limit. + `FrameTransactionFormatError` is raised for any violation. - [EIP-2681]: https://eips.ethereum.org/EIPS/eip-2681 [EIP-8141]: https://eips.ethereum.org/EIPS/eip-8141 """ if len(tx.frames) == 0 or len(tx.frames) > MAX_FRAMES: @@ -1481,15 +1517,6 @@ def validate_frame_transaction(tx: FrameTransaction) -> Uint: "max_fee_per_blob_gas must be zero without blobs" ) - if U256(tx.nonce) >= U256(U64.MAX_VALUE): - raise NonceOverflowError("Nonce too high") - - tx_gas_limit = calculate_frame_transaction_gas_limit(tx) - if calculate_frame_transaction_calldata_floor(tx) > tx_gas_limit: - raise InsufficientTransactionGasError("Insufficient calldata floor") - - return tx_gas_limit - def tx_signature_scheme_is_protocol_validated( sig: TransactionSignature, @@ -1516,46 +1543,44 @@ def signature_verification_gas(sig: TransactionSignature) -> Uint: return Uint(0) -def calculate_frame_transaction_gas_limit(tx: FrameTransaction) -> Uint: +def frame_transaction_charged_data(tx: FrameTransaction) -> Tuple[Bytes, ...]: """ - Calculate the total gas limit of a frame transaction. + Return the byte fields of a frame transaction that are priced as + calldata: the `data` of each frame and the `signer`, `msg`, and + `signature` bytes of each signature entry. The fixed-size fields + are covered by the intrinsic and per-frame costs. + """ + charged: Tuple[Bytes, ...] = () + for frame in tx.frames: + charged += (frame.data,) + for sig in tx.signatures: + charged += (sig.signer, sig.msg, sig.signature) + return charged - The gas limit is the sum of the frame transaction intrinsic cost, - the per-frame cost, the [EIP-7623] calldata cost of the encoded - signature and frame lists, the signature verification cost, and the - gas limits of all frames. - [EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 +def calculate_frame_transaction_gas_limit(tx: FrameTransaction) -> Uint: """ - from .vm.gas import GasCosts + Calculate the total gas limit of a frame transaction. - signature_gas = Uint(0) - for sig in tx.signatures: - signature_gas += signature_verification_gas(sig) + The gas limit is the sum of the transaction's intrinsic cost — see + [`calculate_intrinsic_cost`][cic] — and the gas limits of all frames. - calldata_tokens = count_tokens_in_data( - rlp.encode(tx.signatures) - ) + count_tokens_in_data(rlp.encode(tx.frames)) - calldata_cost = calldata_tokens * GasCosts.TX_DATA_TOKEN_STANDARD + [cic]: ref:ethereum.forks.bogota.transactions.calculate_intrinsic_cost + """ + intrinsic = calculate_intrinsic_cost(tx, tx.sender) total_frame_gas = Uint(0) for frame in tx.frames: total_frame_gas += frame.gas_limit - return ( - FRAME_TX_INTRINSIC_COST - + ulen(tx.frames) * FRAME_TX_PER_FRAME_COST - + calldata_cost - + signature_gas - + total_frame_gas - ) + return Uint(intrinsic.regular) + total_frame_gas def calculate_frame_transaction_calldata_floor(tx: FrameTransaction) -> Uint: """ Calculate the minimum gas cost of a frame transaction based on the - size of the encoded signature and frame lists, per [EIP-7623] and - [EIP-7976]. Like ordinary calldata, every encoded byte counts as a + size of the frame and signature byte fields, per [EIP-7623] and + [EIP-7976]. Like ordinary calldata, every charged byte counts as a standard token and is priced at the floor token cost. [EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 @@ -1563,9 +1588,10 @@ def calculate_frame_transaction_calldata_floor(tx: FrameTransaction) -> Uint: """ from .vm.gas import GasCosts - floor_tokens = ( - ulen(rlp.encode(tx.signatures)) + ulen(rlp.encode(tx.frames)) - ) * GasCosts.TX_DATA_TOKEN_STANDARD + data_length = Uint(0) + for data in frame_transaction_charged_data(tx): + data_length += ulen(data) + floor_tokens = data_length * GasCosts.TX_DATA_TOKEN_STANDARD return ( floor_tokens * GasCosts.TX_DATA_TOKEN_FLOOR + FRAME_TX_INTRINSIC_COST diff --git a/src/ethereum/forks/bogota/utils/message.py b/src/ethereum/forks/bogota/utils/message.py index 2720586d9bf..9a39849dd01 100644 --- a/src/ethereum/forks/bogota/utils/message.py +++ b/src/ethereum/forks/bogota/utils/message.py @@ -18,7 +18,7 @@ from ethereum.state import Address from ..state_tracker import get_account, get_code -from ..transactions import StandardTransaction +from ..transactions import FrameTransaction, Transaction from ..vm import BlockEnvironment, Message, TransactionEnvironment from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS from .address import compute_contract_address @@ -27,11 +27,14 @@ def prepare_message( block_env: BlockEnvironment, tx_env: TransactionEnvironment, - tx: StandardTransaction, + tx: Transaction, ) -> Message: """ Execute a transaction against the provided environment. + The frames of a frame transaction each build their own message, so + frame transactions never reach this function. + Parameters ---------- block_env : @@ -47,6 +50,7 @@ def prepare_message( Items containing contract creation or message call specific data. """ + assert not isinstance(tx, FrameTransaction) accessed_addresses = set() accessed_addresses.add(tx_env.origin) accessed_addresses.update(PRE_COMPILED_CONTRACTS.keys()) From f5c563064554c40a2459ba5cce1d48c74b19be80 Mon Sep 17 00:00:00 2001 From: lightclient Date: Fri, 17 Jul 2026 07:29:14 -0600 Subject: [PATCH 9/9] frames: various fixes --- src/ethereum/forks/bogota/blocks.py | 2 +- src/ethereum/forks/bogota/fork.py | 24 ++++++---- src/ethereum/forks/bogota/transactions.py | 44 +++++++++++++++---- .../bogota/vm/instructions/environment.py | 5 ++- tests/unscheduled/eip8141_frame_tx/spec.py | 2 +- .../test_frame_transactions.py | 2 +- 6 files changed, 59 insertions(+), 20 deletions(-) diff --git a/src/ethereum/forks/bogota/blocks.py b/src/ethereum/forks/bogota/blocks.py index 6a80818d58d..f7a9652c894 100644 --- a/src/ethereum/forks/bogota/blocks.py +++ b/src/ethereum/forks/bogota/blocks.py @@ -405,7 +405,7 @@ class FrameReceipt: status: Uint """ Return code of the top-level call of the frame: 0 for failure, 1 - for success, and 3 for a frame skipped due to a failed atomic + for success, and 2 for a frame skipped due to a failed atomic batch. """ diff --git a/src/ethereum/forks/bogota/fork.py b/src/ethereum/forks/bogota/fork.py index 8320611fbfe..e4e0106e456 100644 --- a/src/ethereum/forks/bogota/fork.py +++ b/src/ethereum/forks/bogota/fork.py @@ -125,6 +125,7 @@ has_access_list, recover_sender, resolve_frame_target, + resolved_signature_signer, validate_frame_signature, validate_transaction, ) @@ -1374,14 +1375,21 @@ def failure(reason: str) -> MessageCallOutput: if allowed_scope & APPROVE_EXECUTION and resolved_target != tx.sender: return failure("execution scope outside sender") - has_sender_signature = any( - sig.scheme == SIGNATURE_SCHEME_SECP256K1 - and sig.signer == resolved_target - and len(sig.msg) == 0 - for sig in tx.signatures + # Frames approving execution authorize with the signature entry at + # index 0; payment-only frames authorize with the entry at index 1. + if allowed_scope & APPROVE_EXECUTION: + sig_index = 0 + else: + sig_index = 1 + has_authorizing_signature = ( + len(tx.signatures) > sig_index + and tx.signatures[sig_index].scheme == SIGNATURE_SCHEME_SECP256K1 + and len(tx.signatures[sig_index].msg) == 0 + and resolved_signature_signer(tx.signatures[sig_index], tx.sender) + == resolved_target ) - if not has_sender_signature: - return failure("no matching secp256k1 signature") + if not has_authorizing_signature: + return failure("no authorizing secp256k1 signature") approval = attempt_frame_approval( frame_context=frame_context, @@ -1583,7 +1591,7 @@ def process_frame_transaction( sig_hash = compute_frame_signature_hash(tx) for sig in tx.signatures: - if not validate_frame_signature(sig, sig_hash): + if not validate_frame_signature(sig, tx.sender, sig_hash): raise FrameTransactionSignatureError("invalid signature entry") blob_gas_fee = calculate_data_fee(block_env.excess_blob_gas, tx) diff --git a/src/ethereum/forks/bogota/transactions.py b/src/ethereum/forks/bogota/transactions.py index 92d6c8f002f..e3c41eea951 100644 --- a/src/ethereum/forks/bogota/transactions.py +++ b/src/ethereum/forks/bogota/transactions.py @@ -247,7 +247,7 @@ class IntrinsicGasCost: Frame receipt status of a frame that executed successfully. """ -FRAME_STATUS_SKIPPED = Uint(3) +FRAME_STATUS_SKIPPED = Uint(2) """ Frame receipt status of a frame that was skipped because an earlier frame of its atomic batch failed. @@ -1454,9 +1454,9 @@ def validate_frame_transaction(tx: FrameTransaction) -> None: for sig in tx.signatures: if tx_signature_scheme_is_protocol_validated(sig): - if len(sig.signer) != 20: + if len(sig.signer) not in (0, 20): raise FrameTransactionFormatError( - "signer must be a 20-byte address" + "signer must be empty or a 20-byte address" ) elif sig.scheme == SIGNATURE_SCHEME_ARBITRARY: if len(sig.signer) != 0: @@ -1492,6 +1492,16 @@ def validate_frame_transaction(tx: FrameTransaction) -> None: if total_frame_gas > Uint(U64.MAX_VALUE): raise FrameTransactionFormatError("total frame gas too high") + # Execution approval is only allowed for frames that resolve to + # the transaction sender. + if ( + frame.flags & APPROVE_EXECUTION + and resolve_frame_target(tx, frame) != tx.sender + ): + raise FrameTransactionFormatError( + "execution approval flag outside sender target" + ) + # An atomic batch must be terminated by a subsequent frame. if frame.flags & ATOMIC_BATCH_FLAG and i + 1 >= len(tx.frames): raise FrameTransactionFormatError( @@ -1617,16 +1627,32 @@ def compute_frame_signature_hash(tx: FrameTransaction) -> Hash32: return keccak256(b"\x06" + rlp.encode(elided_tx)) +def resolved_signature_signer( + sig: TransactionSignature, sender: Address +) -> Address: + """ + Resolve the signer address of a protocol-validated signature entry. + + An empty `signer` resolves to the transaction sender. + """ + if len(sig.signer) == 0: + return sender + return Address(sig.signer) + + def validate_frame_signature( - sig: TransactionSignature, sig_hash: Hash32 + sig: TransactionSignature, sender: Address, sig_hash: Hash32 ) -> bool: """ Validate a single signature entry of a frame transaction. An empty `msg` authorizes the canonical signature hash; a 32-byte `msg` authorizes that explicit digest. `SECP256K1` and `P256` - entries are cryptographically verified against their `signer` - address, while `ARBITRARY` entries are only structurally checked. + entries are cryptographically verified against their resolved + signer address — see [`resolved_signature_signer`][rss] — while + `ARBITRARY` entries are only structurally checked. + + [rss]: ref:ethereum.forks.bogota.transactions.resolved_signature_signer """ if len(sig.msg) == 0: msg = sig_hash @@ -1638,6 +1664,7 @@ def validate_frame_signature( return False if sig.scheme == SIGNATURE_SCHEME_SECP256K1: + signer = resolved_signature_signer(sig, sender) if len(sig.signature) != 65: return False v = U256(sig.signature[0]) @@ -1653,16 +1680,17 @@ def validate_frame_signature( public_key = secp256k1_recover(r, s, v, msg) except InvalidSignatureError: return False - return Bytes(sig.signer) == keccak256(public_key)[12:32] + return Bytes(signer) == keccak256(public_key)[12:32] elif sig.scheme == SIGNATURE_SCHEME_P256: + signer = resolved_signature_signer(sig, sender) if len(sig.signature) != 128: return False r = U256.from_be_bytes(sig.signature[0:32]) s = U256.from_be_bytes(sig.signature[32:64]) qx = U256.from_be_bytes(sig.signature[64:96]) qy = U256.from_be_bytes(sig.signature[96:128]) - if Bytes(sig.signer) != keccak256(sig.signature[64:128])[12:32]: + if Bytes(signer) != keccak256(sig.signature[64:128])[12:32]: return False try: secp256r1_verify(r, s, qx, qy, msg) diff --git a/src/ethereum/forks/bogota/vm/instructions/environment.py b/src/ethereum/forks/bogota/vm/instructions/environment.py index f0e5fb826a6..eed9f1313ef 100644 --- a/src/ethereum/forks/bogota/vm/instructions/environment.py +++ b/src/ethereum/forks/bogota/vm/instructions/environment.py @@ -23,6 +23,7 @@ ATOMIC_BATCH_FLAG, SIGNATURE_SCHEME_ARBITRARY, resolve_frame_target, + resolved_signature_signer, tx_signature_scheme_is_protocol_validated, ) from ...utils.address import to_address_masked @@ -885,7 +886,9 @@ def sigparam(evm: Evm) -> None: raise InvalidParameter( "arbitrary signature entries have no effective signer" ) - result = U256.from_be_bytes(sig.signer) + result = U256.from_be_bytes( + resolved_signature_signer(sig, frame_context.tx.sender) + ) elif param == U256(0x01): result = U256(sig.scheme) elif param == U256(0x02): diff --git a/tests/unscheduled/eip8141_frame_tx/spec.py b/tests/unscheduled/eip8141_frame_tx/spec.py index 8d868883e59..8859c94c509 100644 --- a/tests/unscheduled/eip8141_frame_tx/spec.py +++ b/tests/unscheduled/eip8141_frame_tx/spec.py @@ -53,7 +53,7 @@ class Spec: # Frame receipt statuses STATUS_FAILURE = 0 STATUS_SUCCESS = 1 - STATUS_SKIPPED = 3 + STATUS_SKIPPED = 2 # TXPARAM selectors TXPARAM_TYPE = 0x00 diff --git a/tests/unscheduled/eip8141_frame_tx/test_frame_transactions.py b/tests/unscheduled/eip8141_frame_tx/test_frame_transactions.py index c57a59c6436..fa63c8fc4cf 100644 --- a/tests/unscheduled/eip8141_frame_tx/test_frame_transactions.py +++ b/tests/unscheduled/eip8141_frame_tx/test_frame_transactions.py @@ -220,7 +220,7 @@ def test_atomic_batch_rollback( When the batch terminator reverts, the storage write of the already executed batch frame is unrolled and both frames report failure. When the first batch frame reverts, the remaining batch - frame is skipped with status `0x3` and no gas consumed. In both + frame is skipped with status `0x2` and no gas consumed. In both cases the storage write is discarded. """ sender = pre.fund_eoa()