From 07f75bb2cc7896694541b77b1e5c212c0b3b36e5 Mon Sep 17 00:00:00 2001 From: felipe Date: Tue, 23 Sep 2025 00:07:11 -0600 Subject: [PATCH 01/18] feat(fork): create the amsterdam fork * feat(fork): Create amsterdam fork using new fork tool * fix: manual fixes after creating amsterdam fork from osaka * fix: Use indexing to designate order of unscheduled forks * move amsterdam -> forks/amsterdam --- pyproject.toml | 6 + src/ethereum/fork_criteria.py | 4 +- src/ethereum/forks/amsterdam/__init__.py | 7 + src/ethereum/forks/amsterdam/blocks.py | 404 +++++++ src/ethereum/forks/amsterdam/bloom.py | 86 ++ src/ethereum/forks/amsterdam/exceptions.py | 131 +++ src/ethereum/forks/amsterdam/fork.py | 1046 +++++++++++++++++ src/ethereum/forks/amsterdam/fork_types.py | 79 ++ src/ethereum/forks/amsterdam/requests.py | 190 +++ src/ethereum/forks/amsterdam/state.py | 642 ++++++++++ src/ethereum/forks/amsterdam/transactions.py | 886 ++++++++++++++ src/ethereum/forks/amsterdam/trie.py | 500 ++++++++ .../forks/amsterdam/utils/__init__.py | 3 + src/ethereum/forks/amsterdam/utils/address.py | 91 ++ .../forks/amsterdam/utils/hexadecimal.py | 53 + src/ethereum/forks/amsterdam/utils/message.py | 90 ++ src/ethereum/forks/amsterdam/vm/__init__.py | 190 +++ .../forks/amsterdam/vm/eoa_delegation.py | 207 ++++ src/ethereum/forks/amsterdam/vm/exceptions.py | 140 +++ src/ethereum/forks/amsterdam/vm/gas.py | 387 ++++++ .../amsterdam/vm/instructions/__init__.py | 368 ++++++ .../amsterdam/vm/instructions/arithmetic.py | 374 ++++++ .../amsterdam/vm/instructions/bitwise.py | 268 +++++ .../forks/amsterdam/vm/instructions/block.py | 255 ++++ .../amsterdam/vm/instructions/comparison.py | 178 +++ .../amsterdam/vm/instructions/control_flow.py | 171 +++ .../amsterdam/vm/instructions/environment.py | 597 ++++++++++ .../forks/amsterdam/vm/instructions/keccak.py | 64 + .../forks/amsterdam/vm/instructions/log.py | 88 ++ .../forks/amsterdam/vm/instructions/memory.py | 177 +++ .../forks/amsterdam/vm/instructions/stack.py | 209 ++++ .../amsterdam/vm/instructions/storage.py | 184 +++ .../forks/amsterdam/vm/instructions/system.py | 742 ++++++++++++ .../forks/amsterdam/vm/interpreter.py | 321 +++++ src/ethereum/forks/amsterdam/vm/memory.py | 80 ++ .../vm/precompiled_contracts/__init__.py | 56 + .../vm/precompiled_contracts/alt_bn128.py | 225 ++++ .../vm/precompiled_contracts/blake2f.py | 41 + .../bls12_381/__init__.py | 615 ++++++++++ .../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 | 63 + .../vm/precompiled_contracts/identity.py | 38 + .../vm/precompiled_contracts/mapping.py | 77 ++ .../vm/precompiled_contracts/modexp.py | 178 +++ .../vm/precompiled_contracts/p256verify.py | 85 ++ .../precompiled_contracts/point_evaluation.py | 72 ++ .../vm/precompiled_contracts/ripemd160.py | 43 + .../vm/precompiled_contracts/sha256.py | 40 + src/ethereum/forks/amsterdam/vm/runtime.py | 68 ++ src/ethereum/forks/amsterdam/vm/stack.py | 59 + 52 files changed, 11245 insertions(+), 2 deletions(-) create mode 100644 src/ethereum/forks/amsterdam/__init__.py create mode 100644 src/ethereum/forks/amsterdam/blocks.py create mode 100644 src/ethereum/forks/amsterdam/bloom.py create mode 100644 src/ethereum/forks/amsterdam/exceptions.py create mode 100644 src/ethereum/forks/amsterdam/fork.py create mode 100644 src/ethereum/forks/amsterdam/fork_types.py create mode 100644 src/ethereum/forks/amsterdam/requests.py create mode 100644 src/ethereum/forks/amsterdam/state.py create mode 100644 src/ethereum/forks/amsterdam/transactions.py create mode 100644 src/ethereum/forks/amsterdam/trie.py create mode 100644 src/ethereum/forks/amsterdam/utils/__init__.py create mode 100644 src/ethereum/forks/amsterdam/utils/address.py create mode 100644 src/ethereum/forks/amsterdam/utils/hexadecimal.py create mode 100644 src/ethereum/forks/amsterdam/utils/message.py create mode 100644 src/ethereum/forks/amsterdam/vm/__init__.py create mode 100644 src/ethereum/forks/amsterdam/vm/eoa_delegation.py create mode 100644 src/ethereum/forks/amsterdam/vm/exceptions.py create mode 100644 src/ethereum/forks/amsterdam/vm/gas.py create mode 100644 src/ethereum/forks/amsterdam/vm/instructions/__init__.py create mode 100644 src/ethereum/forks/amsterdam/vm/instructions/arithmetic.py create mode 100644 src/ethereum/forks/amsterdam/vm/instructions/bitwise.py create mode 100644 src/ethereum/forks/amsterdam/vm/instructions/block.py create mode 100644 src/ethereum/forks/amsterdam/vm/instructions/comparison.py create mode 100644 src/ethereum/forks/amsterdam/vm/instructions/control_flow.py create mode 100644 src/ethereum/forks/amsterdam/vm/instructions/environment.py create mode 100644 src/ethereum/forks/amsterdam/vm/instructions/keccak.py create mode 100644 src/ethereum/forks/amsterdam/vm/instructions/log.py create mode 100644 src/ethereum/forks/amsterdam/vm/instructions/memory.py create mode 100644 src/ethereum/forks/amsterdam/vm/instructions/stack.py create mode 100644 src/ethereum/forks/amsterdam/vm/instructions/storage.py create mode 100644 src/ethereum/forks/amsterdam/vm/instructions/system.py create mode 100644 src/ethereum/forks/amsterdam/vm/interpreter.py create mode 100644 src/ethereum/forks/amsterdam/vm/memory.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/__init__.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/alt_bn128.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/blake2f.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/__init__.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g1.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g2.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/ecrecover.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/identity.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/mapping.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/modexp.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/p256verify.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/point_evaluation.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/ripemd160.py create mode 100644 src/ethereum/forks/amsterdam/vm/precompiled_contracts/sha256.py create mode 100644 src/ethereum/forks/amsterdam/vm/runtime.py create mode 100644 src/ethereum/forks/amsterdam/vm/stack.py diff --git a/pyproject.toml b/pyproject.toml index 8f7ba8ce81..47e4b34250 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -226,6 +226,12 @@ packages = [ "ethereum.forks.osaka.vm.instructions", "ethereum.forks.osaka.vm.precompiled_contracts", "ethereum.forks.osaka.vm.precompiled_contracts.bls12_381", + "ethereum.forks.amsterdam", + "ethereum.forks.amsterdam.utils", + "ethereum.forks.amsterdam.vm", + "ethereum.forks.amsterdam.vm.instructions", + "ethereum.forks.amsterdam.vm.precompiled_contracts", + "ethereum.forks.amsterdam.vm.precompiled_contracts.bls12_381", ] [tool.setuptools.package-data] diff --git a/src/ethereum/fork_criteria.py b/src/ethereum/fork_criteria.py index 09729553aa..ccb7bd7afd 100644 --- a/src/ethereum/fork_criteria.py +++ b/src/ethereum/fork_criteria.py @@ -188,8 +188,8 @@ class Unscheduled(ForkCriteria): Forks that have not been scheduled. """ - def __init__(self) -> None: - self._internal = (ForkCriteria.UNSCHEDULED, 0) + def __init__(self, order_index: int = 0) -> None: + self._internal = (ForkCriteria.UNSCHEDULED, order_index) @override def check(self, block_number: Uint, timestamp: U256) -> Literal[False]: diff --git a/src/ethereum/forks/amsterdam/__init__.py b/src/ethereum/forks/amsterdam/__init__.py new file mode 100644 index 0000000000..4719f7430d --- /dev/null +++ b/src/ethereum/forks/amsterdam/__init__.py @@ -0,0 +1,7 @@ +""" +The Amsterdam fork. +""" + +from ethereum.fork_criteria import Unscheduled + +FORK_CRITERIA = Unscheduled(order_index=1) # scheduled after Osaka diff --git a/src/ethereum/forks/amsterdam/blocks.py b/src/ethereum/forks/amsterdam/blocks.py new file mode 100644 index 0000000000..1177e4c32d --- /dev/null +++ b/src/ethereum/forks/amsterdam/blocks.py @@ -0,0 +1,404 @@ +""" +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 + +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 .fork_types import Address, Bloom, Root +from .transactions import ( + AccessListTransaction, + BlobTransaction, + FeeMarketTransaction, + LegacyTransaction, + SetCodeTransaction, + Transaction, +) + + +@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.amsterdam.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. + """ + + +@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.amsterdam.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 the [`state_root()`] function, which computes the root + of the Merkle-Patricia [Trie] representing the Ethereum world state. + + [`keccak256`]: ref:ethereum.crypto.hash.keccak256 + [`state_root()`]: ref:ethereum.forks.amsterdam.state.state_root + [Trie]: ref:ethereum.forks.amsterdam.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.forks.amsterdam.trie.root + [Trie]: ref:ethereum.forks.amsterdam.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.forks.amsterdam.trie.root + [Trie]: ref:ethereum.forks.amsterdam.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.amsterdam.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 also considered when calculating the effective gas limit. This can be + [adjusted by a factor of 1/1024] from the previous block's gas limit, up + until a maximum of 30 million gas. + + [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.amsterdam.requests.compute_requests_hash + [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 + """ + + +@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.amsterdam.blocks.Header + [withdrawals]: ref:ethereum.forks.amsterdam.blocks.Withdrawal + """ + + header: Header + """ + The block header containing metadata and cryptographic commitments. Refer + [headers] for more details on the fields included in the header. + + [headers]: ref:ethereum.forks.amsterdam.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. + """ + + +@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.amsterdam.vm.instructions.log.log0 + [`LOG1`]: ref:ethereum.forks.amsterdam.vm.instructions.log.log1 + [`LOG2`]: ref:ethereum.forks.amsterdam.vm.instructions.log.log2 + [`LOG3`]: ref:ethereum.forks.amsterdam.vm.instructions.log.log3 + [`LOG4`]: ref:ethereum.forks.amsterdam.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. + """ + + +@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. + """ + + 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/amsterdam/bloom.py b/src/ethereum/forks/amsterdam/bloom.py new file mode 100644 index 0000000000..e9ac383a79 --- /dev/null +++ b/src/ethereum/forks/amsterdam/bloom.py @@ -0,0 +1,86 @@ +""" +Ethereum Logs Bloom +^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +This modules 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/amsterdam/exceptions.py b/src/ethereum/forks/amsterdam/exceptions.py new file mode 100644 index 0000000000..3074a1f738 --- /dev/null +++ b/src/ethereum/forks/amsterdam/exceptions.py @@ -0,0 +1,131 @@ +""" +Exceptions specific to this fork. +""" + +from typing import TYPE_CHECKING, Final + +from ethereum_types.numeric import Uint + +from ethereum.exceptions import InvalidTransaction + +if TYPE_CHECKING: + from .transactions import Transaction + + +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. + """ diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py new file mode 100644 index 0000000000..1cd6375b94 --- /dev/null +++ b/src/ethereum/forks/amsterdam/fork.py @@ -0,0 +1,1046 @@ +""" +Ethereum Specification +^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Entry point for the Ethereum specification. +""" + +from dataclasses import dataclass +from typing import List, Optional, Tuple + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.hash import Hash32, keccak256 +from ethereum.exceptions import ( + EthereumException, + GasUsedExceedsLimitError, + InsufficientBalanceError, + InvalidBlock, + InvalidSenderError, + NonceMismatchError, +) + +from . import vm +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, +) +from .fork_types import Account, Address, Authorization, VersionedHash +from .requests import ( + CONSOLIDATION_REQUEST_TYPE, + DEPOSIT_REQUEST_TYPE, + WITHDRAWAL_REQUEST_TYPE, + compute_requests_hash, + parse_deposit_requests, +) +from .state import ( + State, + TransientStorage, + destroy_account, + get_account, + increment_nonce, + modify_state, + set_account_balance, + state_root, +) +from .transactions import ( + AccessListTransaction, + BlobTransaction, + FeeMarketTransaction, + LegacyTransaction, + SetCodeTransaction, + Transaction, + decode_transaction, + encode_transaction, + get_transaction_hash, + recover_sender, + validate_transaction, +) +from .trie import root, trie_set +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 ( + 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) +GAS_LIMIT_ADJUSTMENT_FACTOR = Uint(1024) +GAS_LIMIT_MINIMUM = Uint(5000) +EMPTY_OMMER_HASH = keccak256(rlp.encode([])) +SYSTEM_ADDRESS = hex_to_address("0xfffffffffffffffffffffffffffffffffffffffe") +BEACON_ROOTS_ADDRESS = hex_to_address( + "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02" +) +SYSTEM_TRANSACTION_GAS = Uint(30000000) +MAX_BLOB_GAS_PER_BLOCK = U64(1179648) +VERSIONED_HASH_VERSION_KZG = b"\x01" + +WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( + "0x00000961Ef480Eb55e80D19ad83579A64c007002" +) +CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = hex_to_address( + "0x0000BBdDc7CE488642fb579F8B00f3a590007251" +) +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 + + +@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`. + """ + if len(rlp.encode(block)) > MAX_RLP_BLOCK_SIZE: + raise InvalidBlock("Block rlp size exceeds MAX_RLP_BLOCK_SIZE") + + validate_header(chain, block.header) + if block.ommers != (): + raise InvalidBlock + + block_env = vm.BlockEnvironment( + chain_id=chain.chain_id, + state=chain.state, + block_gas_limit=block.header.gas_limit, + block_hashes=get_last_256_block_hashes(chain), + 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_output = apply_body( + block_env=block_env, + transactions=block.transactions, + withdrawals=block.withdrawals, + ) + block_state_root = state_root(block_env.state) + 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) + + if block_output.block_gas_used != block.header.gas_used: + raise InvalidBlock( + f"{block_output.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 + + 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 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(chain: BlockChain, header: Header) -> None: + """ + Verifies a block header. + + 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 + ---------- + chain : + History and current state. + header : + Header to check for correctness. + """ + if header.number < Uint(1): + raise InvalidBlock + + parent_header = chain.blocks[-1].header + + 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, +) -> Tuple[Address, 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. + + Returns + ------- + sender_address : + The sender of the transaction. + 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. + """ + gas_available = block_env.block_gas_limit - block_output.block_gas_used + blob_gas_available = MAX_BLOB_GAS_PER_BLOCK - block_output.blob_gas_used + + if tx.gas > gas_available: + raise GasUsedExceedsLimitError("gas used exceeds limit") + + tx_blob_gas_used = calculate_total_blob_gas(tx) + if tx_blob_gas_used > blob_gas_available: + raise BlobGasLimitExceededError("blob gas limit exceeded") + + sender_address = recover_sender(block_env.chain_id, tx) + sender_account = get_account(block_env.state, sender_address) + + if isinstance( + tx, (FeeMarketTransaction, BlobTransaction, SetCodeTransaction) + ): + 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") + if sender_account.code and not is_valid_delegation(sender_account.code): + raise InvalidSenderError("not EOA") + + return ( + sender_address, + 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. + 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_system_transaction( + block_env: vm.BlockEnvironment, + target_address: Address, + system_contract_code: Bytes, + data: Bytes, +) -> MessageCallOutput: + """ + Process a system transaction with the given code. + + Prefer calling `process_checked_system_transaction` or + `process_unchecked_system_transaction` depending on whether missing code or + an execution error should cause the block to be rejected. + + Parameters + ---------- + block_env : + The block scoped environment. + target_address : + Address of the contract to call. + system_contract_code : + Code of the contract to call. + data : + Data to pass to the contract. + + Returns + ------- + system_tx_output : `MessageCallOutput` + Output of processing the system transaction. + """ + tx_env = vm.TransactionEnvironment( + origin=SYSTEM_ADDRESS, + gas_price=block_env.base_fee_per_gas, + gas=SYSTEM_TRANSACTION_GAS, + access_list_addresses=set(), + access_list_storage_keys=set(), + transient_storage=TransientStorage(), + blob_versioned_hashes=(), + authorizations=(), + index_in_block=None, + tx_hash=None, + ) + + system_tx_message = Message( + block_env=block_env, + tx_env=tx_env, + caller=SYSTEM_ADDRESS, + target=target_address, + gas=SYSTEM_TRANSACTION_GAS, + 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) + + return system_tx_output + + +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. + """ + system_contract_code = get_account(block_env.state, target_address).code + + if len(system_contract_code) == 0: + raise InvalidBlock( + f"System contract address {target_address.hex()} does not " + "contain code" + ) + + system_tx_output = process_system_transaction( + block_env, + target_address, + system_contract_code, + 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_contract_code = get_account(block_env.state, target_address).code + return process_system_transaction( + block_env, + target_address, + system_contract_code, + data, + ) + + +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)) + + process_withdrawals(block_env, block_output, withdrawals) + + process_general_purpose_requests( + block_env=block_env, + block_output=block_output, + ) + + 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 + ) + + +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. + """ + trie_set( + block_output.transactions_trie, + rlp.encode(index), + encode_transaction(tx), + ) + + intrinsic_gas, calldata_floor_gas_cost = validate_transaction(tx) + + ( + sender, + effective_gas_price, + blob_versioned_hashes, + tx_blob_gas_used, + ) = check_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + ) + + sender_account = get_account(block_env.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 + + gas = tx.gas - intrinsic_gas + increment_nonce(block_env.state, sender) + + sender_balance_after_gas_fee = ( + Uint(sender_account.balance) - effective_gas_fee - blob_gas_fee + ) + set_account_balance( + block_env.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 isinstance( + tx, + ( + AccessListTransaction, + FeeMarketTransaction, + BlobTransaction, + SetCodeTransaction, + ), + ): + 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, + gas_price=effective_gas_price, + gas=gas, + access_list_addresses=access_list_addresses, + access_list_storage_keys=access_list_storage_keys, + transient_storage=TransientStorage(), + blob_versioned_hashes=blob_versioned_hashes, + authorizations=authorizations, + index_in_block=index, + tx_hash=get_transaction_hash(encode_transaction(tx)), + ) + + message = prepare_message(block_env, tx_env, tx) + + tx_output = process_message_call(message) + + # For EIP-7623 we first calculate the execution_gas_used, which includes + # the execution gas refund. + tx_gas_used_before_refund = tx.gas - tx_output.gas_left + tx_gas_refund = min( + tx_gas_used_before_refund // Uint(5), Uint(tx_output.refund_counter) + ) + tx_gas_used_after_refund = tx_gas_used_before_refund - tx_gas_refund + + # Transactions with less execution_gas_used than the floor pay at the + # floor cost. + tx_gas_used_after_refund = max( + tx_gas_used_after_refund, calldata_floor_gas_cost + ) + + tx_gas_left = tx.gas - tx_gas_used_after_refund + 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_after_refund * priority_fee_per_gas + + # refund gas + sender_balance_after_refund = get_account( + block_env.state, sender + ).balance + U256(gas_refund_amount) + set_account_balance(block_env.state, sender, sender_balance_after_refund) + + # transfer miner fees + coinbase_balance_after_mining_fee = get_account( + block_env.state, block_env.coinbase + ).balance + U256(transaction_fee) + set_account_balance( + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, + ) + + for address in tx_output.accounts_to_delete: + destroy_account(block_env.state, address) + + block_output.block_gas_used += tx_gas_used_after_refund + block_output.blob_gas_used += tx_blob_gas_used + + receipt = make_receipt( + tx, tx_output.error, block_output.block_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 + + +def process_withdrawals( + block_env: vm.BlockEnvironment, + block_output: vm.BlockOutput, + withdrawals: Tuple[Withdrawal, ...], +) -> None: + """ + Increase the balance of the withdrawing account. + """ + + def increase_recipient_balance(recipient: Account) -> None: + recipient.balance += wd.amount * U256(10**9) + + for i, wd in enumerate(withdrawals): + trie_set( + block_output.withdrawals_trie, + rlp.encode(Uint(i)), + rlp.encode(wd), + ) + + modify_state(block_env.state, wd.address, increase_recipient_balance) + + +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 + ``GAS_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 ``GAS_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 // GAS_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 < GAS_LIMIT_MINIMUM: + return False + + return True diff --git a/src/ethereum/forks/amsterdam/fork_types.py b/src/ethereum/forks/amsterdam/fork_types.py new file mode 100644 index 0000000000..5d54e36b11 --- /dev/null +++ b/src/ethereum/forks/amsterdam/fork_types.py @@ -0,0 +1,79 @@ +""" +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 ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes20, Bytes256 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U8, U64, U256, Uint + +from ethereum.crypto.hash import Hash32, keccak256 + +Address = Bytes20 +Root = Hash32 +VersionedHash = Hash32 + +Bloom = Bytes256 + + +@slotted_freezable +@dataclass +class Account: + """ + State associated with an address. + """ + + nonce: Uint + balance: U256 + code: Bytes + + +EMPTY_ACCOUNT = Account( + nonce=Uint(0), + balance=U256(0), + code=b"", +) + + +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, + keccak256(raw_account_data.code), + ) + ) + + +@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/amsterdam/requests.py b/src/ethereum/forks/amsterdam/requests.py new file mode 100644 index 0000000000..6ed423188f --- /dev/null +++ b/src/ethereum/forks/amsterdam/requests.py @@ -0,0 +1,190 @@ +""" +Requests were introduced in EIP-7685 as a a general purpose framework for +storing contract-triggered requests. It extends the execution header and +body with a single field each to store the request information. +This inherently exposes the requests to the consensus layer, which can +then process each one. + +[EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 +""" + +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.utils.hexadecimal import hex_to_bytes32 + +from .blocks import decode_receipt +from .trie import trie_get +from .utils.hexadecimal import hex_to_address +from .vm import BlockOutput + +DEPOSIT_CONTRACT_ADDRESS = hex_to_address( + "0x00000000219ab540356cbb839cbe05303d7705fa" +) +DEPOSIT_EVENT_SIGNATURE_HASH = hex_to_bytes32( + "0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5" +) +DEPOSIT_REQUEST_TYPE = b"\x00" +WITHDRAWAL_REQUEST_TYPE = b"\x01" +CONSOLIDATION_REQUEST_TYPE = b"\x02" + + +DEPOSIT_EVENT_LENGTH = Uint(576) + +PUBKEY_OFFSET = Uint(160) +WITHDRAWAL_CREDENTIALS_OFFSET = Uint(256) +AMOUNT_OFFSET = Uint(320) +SIGNATURE_OFFSET = Uint(384) +INDEX_OFFSET = Uint(512) + +PUBKEY_SIZE = Uint(48) +WITHDRAWAL_CREDENTIALS_SIZE = Uint(32) +AMOUNT_SIZE = Uint(8) +SIGNATURE_SIZE = Uint(96) +INDEX_SIZE = Uint(8) + + +def extract_deposit_data(data: Bytes) -> Bytes: + """ + Extracts Deposit Request from the DepositContract.DepositEvent data. + + Raises + ------ + InvalidBlock : + If the deposit contract did not produce a valid log. + """ + 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: + """ + Parse deposit requests from the block output. + """ + 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: + """ + Get the hash of the requests using the SHA2-256 algorithm. + + Parameters + ---------- + requests : Bytes + The requests to hash. + + Returns + ------- + requests_hash : Bytes + The hash of the requests. + """ + m = sha256() + for request in requests: + m.update(sha256(request).digest()) + + return m.digest() diff --git a/src/ethereum/forks/amsterdam/state.py b/src/ethereum/forks/amsterdam/state.py new file mode 100644 index 0000000000..7cec40084d --- /dev/null +++ b/src/ethereum/forks/amsterdam/state.py @@ -0,0 +1,642 @@ +""" +State +^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The state contains all information that is preserved between transactions. + +It consists of a main account trie and storage tries for each contract. + +There is a distinction between an account that does not exist and +`EMPTY_ACCOUNT`. +""" + +from dataclasses import dataclass, field +from typing import Callable, Dict, List, Optional, Set, Tuple + +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.frozen import modify +from ethereum_types.numeric import U256, Uint + +from .fork_types import EMPTY_ACCOUNT, Account, Address, Root +from .trie import EMPTY_TRIE_ROOT, Trie, copy_trie, root, trie_get, trie_set + + +@dataclass +class State: + """ + Contains all information that is preserved between transactions. + """ + + _main_trie: Trie[Address, Optional[Account]] = field( + default_factory=lambda: Trie(secured=True, default=None) + ) + _storage_tries: Dict[Address, Trie[Bytes32, U256]] = field( + default_factory=dict + ) + _snapshots: List[ + Tuple[ + Trie[Address, Optional[Account]], + Dict[Address, Trie[Bytes32, U256]], + ] + ] = field(default_factory=list) + created_accounts: Set[Address] = field(default_factory=set) + + +@dataclass +class TransientStorage: + """ + Contains all information that is preserved between message calls + within a transaction. + """ + + _tries: Dict[Address, Trie[Bytes32, U256]] = field(default_factory=dict) + _snapshots: List[Dict[Address, Trie[Bytes32, U256]]] = field( + default_factory=list + ) + + +def close_state(state: State) -> None: + """ + Free resources held by the state. Used by optimized implementations to + release file descriptors. + """ + del state._main_trie + del state._storage_tries + del state._snapshots + del state.created_accounts + + +def begin_transaction( + state: State, transient_storage: TransientStorage +) -> None: + """ + Start a state transaction. + + Transactions are entirely implicit and can be nested. It is not possible to + calculate the state root during a transaction. + + Parameters + ---------- + state : State + The state. + transient_storage : TransientStorage + The transient storage of the transaction. + """ + state._snapshots.append( + ( + copy_trie(state._main_trie), + {k: copy_trie(t) for (k, t) in state._storage_tries.items()}, + ) + ) + transient_storage._snapshots.append( + {k: copy_trie(t) for (k, t) in transient_storage._tries.items()} + ) + + +def commit_transaction( + state: State, transient_storage: TransientStorage +) -> None: + """ + Commit a state transaction. + + Parameters + ---------- + state : State + The state. + transient_storage : TransientStorage + The transient storage of the transaction. + """ + state._snapshots.pop() + if not state._snapshots: + state.created_accounts.clear() + + transient_storage._snapshots.pop() + + +def rollback_transaction( + state: State, transient_storage: TransientStorage +) -> None: + """ + Rollback a state transaction, resetting the state to the point when the + corresponding `begin_transaction()` call was made. + + Parameters + ---------- + state : State + The state. + transient_storage : TransientStorage + The transient storage of the transaction. + """ + state._main_trie, state._storage_tries = state._snapshots.pop() + if not state._snapshots: + state.created_accounts.clear() + + transient_storage._tries = transient_storage._snapshots.pop() + + +def get_account(state: State, address: Address) -> Account: + """ + Get the `Account` object at an address. Returns `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 + ---------- + state: `State` + The state + address : `Address` + Address to lookup. + + Returns + ------- + account : `Account` + Account at address. + """ + account = get_account_optional(state, address) + if isinstance(account, Account): + return account + else: + return EMPTY_ACCOUNT + + +def get_account_optional(state: State, address: Address) -> Optional[Account]: + """ + Get the `Account` object at an address. Returns `None` (rather than + `EMPTY_ACCOUNT`) if there is no account at the address. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address to lookup. + + Returns + ------- + account : `Account` + Account at address. + """ + account = trie_get(state._main_trie, address) + return account + + +def set_account( + state: State, 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 + ---------- + state: `State` + The state + address : `Address` + Address to set. + account : `Account` + Account to set at address. + """ + trie_set(state._main_trie, address, account) + + +def destroy_account(state: State, address: Address) -> None: + """ + Completely remove the account at `address` and all of its storage. + + This function is made available exclusively for the `SELFDESTRUCT` + opcode. It is expected that `SELFDESTRUCT` will be disabled in a future + hardfork and this function will be removed. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of account to destroy. + """ + destroy_storage(state, address) + set_account(state, address, None) + + +def destroy_storage(state: State, address: Address) -> None: + """ + Completely remove the storage at `address`. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of account whose storage is to be deleted. + """ + if address in state._storage_tries: + del state._storage_tries[address] + + +def mark_account_created(state: State, 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 + ---------- + state: `State` + The state + address : `Address` + Address of the account that has been created. + """ + state.created_accounts.add(address) + + +def get_storage(state: State, address: Address, key: Bytes32) -> U256: + """ + Get a value at a storage key on an account. Returns `U256(0)` if the + storage key has not been set previously. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of the account. + key : `Bytes` + Key to lookup. + + Returns + ------- + value : `U256` + Value at the key. + """ + trie = state._storage_tries.get(address) + if trie is None: + return U256(0) + + value = trie_get(trie, key) + + assert isinstance(value, U256) + return value + + +def set_storage( + state: State, address: Address, key: Bytes32, value: U256 +) -> None: + """ + Set a value at a storage key on an account. Setting to `U256(0)` deletes + the key. + + Parameters + ---------- + state: `State` + The state + address : `Address` + Address of the account. + key : `Bytes` + Key to set. + value : `U256` + Value to set at the key. + """ + assert trie_get(state._main_trie, address) is not None + + trie = state._storage_tries.get(address) + if trie is None: + trie = Trie(secured=True, default=U256(0)) + state._storage_tries[address] = trie + trie_set(trie, key, value) + if trie._data == {}: + del state._storage_tries[address] + + +def storage_root(state: State, address: Address) -> Root: + """ + Calculate the storage root of an account. + + Parameters + ---------- + state: + The state + address : + Address of the account. + + Returns + ------- + root : `Root` + Storage root of the account. + """ + assert not state._snapshots + if address in state._storage_tries: + return root(state._storage_tries[address]) + else: + return EMPTY_TRIE_ROOT + + +def state_root(state: State) -> Root: + """ + Calculate the state root. + + Parameters + ---------- + state: + The current state. + + Returns + ------- + root : `Root` + The state root. + """ + assert not state._snapshots + + def get_storage_root(address: Address) -> Root: + return storage_root(state, address) + + return root(state._main_trie, get_storage_root=get_storage_root) + + +def account_exists(state: State, address: Address) -> bool: + """ + Checks if an account exists in the state trie + + Parameters + ---------- + state: + The 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(state, address) is not None + + +def account_has_code_or_nonce(state: State, address: Address) -> bool: + """ + Checks if an account has non zero nonce or non empty code + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + has_code_or_nonce : `bool` + True if the account has non zero nonce or non empty code, + False otherwise. + """ + account = get_account(state, address) + return account.nonce != Uint(0) or account.code != b"" + + +def account_has_storage(state: State, address: Address) -> bool: + """ + Checks if an account has storage. + + Parameters + ---------- + state: + The state + address: + Address of the account that needs to be checked. + + Returns + ------- + has_storage : `bool` + True if the account has storage, False otherwise. + """ + return address in state._storage_tries + + +def is_account_alive(state: State, address: Address) -> bool: + """ + Check whether an account is both in the state and non-empty. + + Parameters + ---------- + state: + The 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(state, address) + return account is not None and account != EMPTY_ACCOUNT + + +def modify_state( + state: State, 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(state, address, modify(get_account(state, address), f)) + + account = get_account_optional(state, address) + account_exists_and_is_empty = ( + account is not None + and account.nonce == Uint(0) + and account.code == b"" + and account.balance == 0 + ) + + if account_exists_and_is_empty: + destroy_account(state, address) + + +def move_ether( + state: State, + sender_address: Address, + recipient_address: Address, + amount: U256, +) -> None: + """ + Move funds between accounts. + """ + + 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(state, sender_address, reduce_sender_balance) + modify_state(state, recipient_address, increase_recipient_balance) + + +def set_account_balance(state: State, address: Address, amount: U256) -> None: + """ + Sets the balance of an account. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose nonce needs to be incremented. + + amount: + The amount that needs to set in balance. + """ + + def set_balance(account: Account) -> None: + account.balance = amount + + modify_state(state, address, set_balance) + + +def increment_nonce(state: State, address: Address) -> None: + """ + Increments the nonce of an account. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose nonce needs to be incremented. + """ + + def increase_nonce(sender: Account) -> None: + sender.nonce += Uint(1) + + modify_state(state, address, increase_nonce) + + +def set_code(state: State, address: Address, code: Bytes) -> None: + """ + Sets Account code. + + Parameters + ---------- + state: + The current state. + + address: + Address of the account whose code needs to be update. + + code: + The bytecode that needs to be set. + """ + + def write_code(sender: Account) -> None: + sender.code = code + + modify_state(state, address, write_code) + + +def get_storage_original(state: State, address: Address, key: Bytes32) -> U256: + """ + Get the original value in a storage slot i.e. the value before the current + transaction began. This function reads the value from the snapshots taken + before executing the transaction. + + Parameters + ---------- + state: + The current state. + address: + Address of the account to read the value from. + key: + Key of the storage slot. + """ + # In the transaction where an account is created, its preexisting storage + # is ignored. + if address in state.created_accounts: + return U256(0) + + _, original_trie = state._snapshots[0] + original_account_trie = original_trie.get(address) + + if original_account_trie is None: + original_value = U256(0) + else: + original_value = trie_get(original_account_trie, key) + + assert isinstance(original_value, U256) + + return original_value + + +def get_transient_storage( + transient_storage: TransientStorage, address: Address, key: Bytes32 +) -> U256: + """ + Get a value at a storage key on an account from transient storage. + Returns `U256(0)` if the storage key has not been set previously. + Parameters + ---------- + transient_storage: `TransientStorage` + The transient storage + address : `Address` + Address of the account. + key : `Bytes` + Key to lookup. + Returns + ------- + value : `U256` + Value at the key. + """ + trie = transient_storage._tries.get(address) + if trie is None: + return U256(0) + + value = trie_get(trie, key) + + assert isinstance(value, U256) + return value + + +def set_transient_storage( + transient_storage: TransientStorage, + address: Address, + key: Bytes32, + value: U256, +) -> None: + """ + Set a value at a storage key on an account. Setting to `U256(0)` deletes + the key. + Parameters + ---------- + transient_storage: `TransientStorage` + The transient storage + address : `Address` + Address of the account. + key : `Bytes` + Key to set. + value : `U256` + Value to set at the key. + """ + trie = transient_storage._tries.get(address) + if trie is None: + trie = Trie(secured=True, default=U256(0)) + transient_storage._tries[address] = trie + trie_set(trie, key, value) + if trie._data == {}: + del transient_storage._tries[address] diff --git a/src/ethereum/forks/amsterdam/transactions.py b/src/ethereum/forks/amsterdam/transactions.py new file mode 100644 index 0000000000..90b1deecd7 --- /dev/null +++ b/src/ethereum/forks/amsterdam/transactions.py @@ -0,0 +1,886 @@ +""" +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 + +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 .exceptions import ( + InitCodeTooLargeError, + TransactionGasLimitExceededError, + TransactionTypeError, +) +from .fork_types import Address, Authorization, VersionedHash + +TX_BASE_COST = Uint(21000) +""" +Base cost of a transaction in gas units. This is the minimum amount of gas +required to execute a transaction. +""" + +FLOOR_CALLDATA_COST = Uint(10) +""" +Minimum gas cost per byte of calldata as per [EIP-7623]. Used to calculate +the minimum gas cost for transactions that include calldata. + +[EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 +""" + +STANDARD_CALLDATA_TOKEN_COST = Uint(4) +""" +Gas cost per byte of calldata as per [EIP-7623]. Used to calculate the +gas cost for transactions that include calldata. + +[EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 +""" + +TX_CREATE_COST = Uint(32000) +""" +Additional gas cost for creating a new contract. +""" + +TX_ACCESS_LIST_ADDRESS_COST = Uint(2400) +""" +Gas cost for including an address in the access list of a transaction. +""" + +TX_ACCESS_LIST_STORAGE_KEY_COST = Uint(1900) +""" +Gas cost for including a storage key in the access list of a transaction. +""" + +TX_MAX_GAS_LIMIT = Uint(16_777_216) + + +@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. + """ + + +@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. + """ + + +@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. + """ + + +@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. + """ + + +@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. + """ + + +@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. +""" + + +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. + + Legacy transactions are returned as-is, while other transaction types + are decoded based on their type identifier 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:]) + else: + raise TransactionTypeError(tx[0]) + else: + return tx + + +def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]: + """ + 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 as a parameter and returns the intrinsic + gas cost and the minimum calldata gas cost for the transaction after + validation. It throws an `InsufficientTransactionGasError` exception if + the transaction does not provide enough gas to cover the intrinsic cost, + and a `NonceOverflowError` exception if the nonce is greater than + `2**64 - 2`. It also raises an `InitCodeTooLargeError` if the code size of + a contract creation transaction exceeds the maximum allowed size. + + [EIP-2681]: https://eips.ethereum.org/EIPS/eip-2681 + [EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 + """ + from .vm.interpreter import MAX_INIT_CODE_SIZE + + intrinsic_gas, calldata_floor_gas_cost = calculate_intrinsic_cost(tx) + if max(intrinsic_gas, calldata_floor_gas_cost) > tx.gas: + raise InsufficientTransactionGasError("Insufficient gas") + if U256(tx.nonce) >= U256(U64.MAX_VALUE): + raise NonceOverflowError("Nonce too high") + if tx.to == Bytes0(b"") and len(tx.data) > MAX_INIT_CODE_SIZE: + raise InitCodeTooLargeError("Code size too large") + if tx.gas > TX_MAX_GAS_LIMIT: + raise TransactionGasLimitExceededError("Gas limit too high") + + return intrinsic_gas, calldata_floor_gas_cost + + +def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: + """ + 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. Base cost (`TX_BASE_COST`) + 2. Cost for data (zero and non-zero bytes) + 3. Cost for contract creation (if applicable) + 4. Cost for access list entries (if applicable) + 5. Cost for authorizations (if applicable) + + + This function takes a transaction as a parameter and returns the intrinsic + gas cost of the transaction and the minimum gas cost used by the + transaction based on the calldata size. + """ + from .vm.eoa_delegation import PER_EMPTY_ACCOUNT_COST + from .vm.gas import init_code_cost + + zero_bytes = 0 + for byte in tx.data: + if byte == 0: + zero_bytes += 1 + + tokens_in_calldata = Uint(zero_bytes + (len(tx.data) - zero_bytes) * 4) + # EIP-7623 floor price (note: no EVM costs) + calldata_floor_gas_cost = ( + tokens_in_calldata * FLOOR_CALLDATA_COST + TX_BASE_COST + ) + + data_cost = tokens_in_calldata * STANDARD_CALLDATA_TOKEN_COST + + if tx.to == Bytes0(b""): + create_cost = TX_CREATE_COST + init_code_cost(ulen(tx.data)) + else: + create_cost = Uint(0) + + access_list_cost = Uint(0) + if isinstance( + tx, + ( + AccessListTransaction, + FeeMarketTransaction, + BlobTransaction, + SetCodeTransaction, + ), + ): + for access in tx.access_list: + access_list_cost += TX_ACCESS_LIST_ADDRESS_COST + access_list_cost += ( + ulen(access.slots) * TX_ACCESS_LIST_STORAGE_KEY_COST + ) + + auth_cost = Uint(0) + if isinstance(tx, SetCodeTransaction): + auth_cost += Uint(PER_EMPTY_ACCOUNT_COST * len(tx.authorizations)) + + return ( + Uint( + TX_BASE_COST + + data_cost + + create_cost + + access_list_cost + + auth_cost + ), + calldata_floor_gas_cost, + ) + + +def recover_sender(chain_id: U64, 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: + chain_id_x2 = U256(chain_id) * U256(2) + if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: + raise InvalidSignatureError("bad v") + public_key = secp256k1_recover( + r, + s, + v - U256(35) - chain_id_x2, + signing_hash_155(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) diff --git a/src/ethereum/forks/amsterdam/trie.py b/src/ethereum/forks/amsterdam/trie.py new file mode 100644 index 0000000000..44953a90a2 --- /dev/null +++ b/src/ethereum/forks/amsterdam/trie.py @@ -0,0 +1,500 @@ +""" +State Trie +^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +The state trie is the structure responsible for storing +`.fork_types.Account` objects. +""" + +import copy +from dataclasses import dataclass, field +from typing import ( + Callable, + Dict, + Generic, + List, + Mapping, + MutableMapping, + Optional, + Sequence, + Tuple, + TypeVar, + cast, +) + +from ethereum_rlp import Extended, rlp +from ethereum_types.bytes import Bytes +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U256, Uint +from typing_extensions import assert_type + +from ethereum.crypto.hash import keccak256 +from ethereum.forks.osaka import trie as previous_trie +from ethereum.utils.hexadecimal import hex_to_bytes + +from .blocks import Receipt, Withdrawal +from .fork_types import Account, Address, Root, encode_account +from .transactions import LegacyTransaction + +# note: an empty trie (regardless of whether it is secured) has root: +# +# keccak256(RLP(b'')) +# == +# 56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 # noqa: E501 +# +# also: +# +# keccak256(RLP(())) +# == +# 1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347 # noqa: E501 +# +# which is the sha3Uncles hash in block header with no uncles +EMPTY_TRIE_ROOT = Root( + hex_to_bytes( + "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" + ) +) + +Node = ( + Account + | Bytes + | LegacyTransaction + | Receipt + | Uint + | U256 + | Withdrawal + | None +) +K = TypeVar("K", bound=Bytes) +V = TypeVar( + "V", + Optional[Account], + Optional[Bytes], + Bytes, + Optional[LegacyTransaction | Bytes], + Optional[Receipt | Bytes], + Optional[Withdrawal | Bytes], + Uint, + U256, +) + + +@slotted_freezable +@dataclass +class LeafNode: + """Leaf node in the Merkle Trie""" + + rest_of_key: Bytes + value: Extended + + +@slotted_freezable +@dataclass +class ExtensionNode: + """Extension node in the Merkle Trie""" + + key_segment: Bytes + subnode: Extended + + +BranchSubnodes = Tuple[ + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, + Extended, +] + + +@slotted_freezable +@dataclass +class BranchNode: + """Branch node in the Merkle Trie""" + + subnodes: BranchSubnodes + value: Extended + + +InternalNode = LeafNode | ExtensionNode | BranchNode + + +def encode_internal_node(node: Optional[InternalNode]) -> Extended: + """ + Encodes a Merkle Trie node into its RLP form. The RLP will then be + serialized into a `Bytes` and hashed unless it is less that 32 bytes + when serialized. + + This function also accepts `None`, representing the absence of a node, + which is encoded to `b""`. + + Parameters + ---------- + node : Optional[InternalNode] + The node to encode. + + Returns + ------- + encoded : `Extended` + The node encoded as RLP. + """ + unencoded: Extended + if node is None: + unencoded = b"" + elif isinstance(node, LeafNode): + unencoded = ( + nibble_list_to_compact(node.rest_of_key, True), + node.value, + ) + elif isinstance(node, ExtensionNode): + unencoded = ( + nibble_list_to_compact(node.key_segment, False), + node.subnode, + ) + elif isinstance(node, BranchNode): + unencoded = list(node.subnodes) + [node.value] + else: + raise AssertionError(f"Invalid internal node type {type(node)}!") + + encoded = rlp.encode(unencoded) + if len(encoded) < 32: + return unencoded + else: + return keccak256(encoded) + + +def encode_node(node: Node, storage_root: Optional[Bytes] = None) -> Bytes: + """ + Encode a Node for storage in the Merkle Trie. + + Currently mostly an unimplemented stub. + """ + if isinstance(node, Account): + assert storage_root is not None + return encode_account(node, storage_root) + elif isinstance(node, (LegacyTransaction, Receipt, Withdrawal, U256)): + return rlp.encode(node) + elif isinstance(node, Bytes): + return node + else: + return previous_trie.encode_node(node, storage_root) + + +@dataclass +class Trie(Generic[K, V]): + """ + The Merkle Trie. + """ + + secured: bool + default: V + _data: Dict[K, V] = field(default_factory=dict) + + +def copy_trie(trie: Trie[K, V]) -> Trie[K, V]: + """ + Create a copy of `trie`. Since only frozen objects may be stored in tries, + the contents are reused. + + Parameters + ---------- + trie: `Trie` + Trie to copy. + + Returns + ------- + new_trie : `Trie[K, V]` + A copy of the trie. + """ + return Trie(trie.secured, trie.default, copy.copy(trie._data)) + + +def trie_set(trie: Trie[K, V], key: K, value: V) -> None: + """ + Stores an item in a Merkle Trie. + + This method deletes the key if `value == trie.default`, because the Merkle + Trie represents the default value by omitting it from the trie. + + Parameters + ---------- + trie: `Trie` + Trie to store in. + key : `Bytes` + Key to lookup. + value : `V` + Node to insert at `key`. + """ + if value == trie.default: + if key in trie._data: + del trie._data[key] + else: + trie._data[key] = value + + +def trie_get(trie: Trie[K, V], key: K) -> V: + """ + Gets an item from the Merkle Trie. + + This method returns `trie.default` if the key is missing. + + Parameters + ---------- + trie: + Trie to lookup in. + key : + Key to lookup. + + Returns + ------- + node : `V` + Node at `key` in the trie. + """ + return trie._data.get(key, trie.default) + + +def common_prefix_length(a: Sequence, b: Sequence) -> int: + """ + Find the longest common prefix of two sequences. + """ + for i in range(len(a)): + if i >= len(b) or a[i] != b[i]: + return i + return len(a) + + +def nibble_list_to_compact(x: Bytes, is_leaf: bool) -> Bytes: + """ + Compresses nibble-list into a standard byte array with a flag. + + A nibble-list is a list of byte values no greater than `15`. The flag is + encoded in high nibble of the highest byte. The flag nibble can be broken + down into two two-bit flags. + + Highest nibble:: + + +---+---+----------+--------+ + | _ | _ | is_leaf | parity | + +---+---+----------+--------+ + 3 2 1 0 + + + The lowest bit of the nibble encodes the parity of the length of the + remaining nibbles -- `0` when even and `1` when odd. The second lowest bit + is used to distinguish leaf and extension nodes. The other two bits are not + used. + + Parameters + ---------- + x : + Array of nibbles. + is_leaf : + True if this is part of a leaf node, or false if it is an extension + node. + + Returns + ------- + compressed : `bytearray` + Compact byte array. + """ + compact = bytearray() + + if len(x) % 2 == 0: # ie even length + compact.append(16 * (2 * is_leaf)) + for i in range(0, len(x), 2): + compact.append(16 * x[i] + x[i + 1]) + else: + compact.append(16 * ((2 * is_leaf) + 1) + x[0]) + for i in range(1, len(x), 2): + compact.append(16 * x[i] + x[i + 1]) + + return Bytes(compact) + + +def bytes_to_nibble_list(bytes_: Bytes) -> Bytes: + """ + Converts a `Bytes` into to a sequence of nibbles (bytes with value < 16). + + Parameters + ---------- + bytes_: + The `Bytes` to convert. + + Returns + ------- + nibble_list : `Bytes` + The `Bytes` in nibble-list format. + """ + nibble_list = bytearray(2 * len(bytes_)) + for byte_index, byte in enumerate(bytes_): + nibble_list[byte_index * 2] = (byte & 0xF0) >> 4 + nibble_list[byte_index * 2 + 1] = byte & 0x0F + return Bytes(nibble_list) + + +def _prepare_trie( + trie: Trie[K, V], + get_storage_root: Optional[Callable[[Address], Root]] = None, +) -> Mapping[Bytes, Bytes]: + """ + Prepares the trie for root calculation. Removes values that are empty, + hashes the keys (if `secured == True`) and encodes all the nodes. + + Parameters + ---------- + trie : + The `Trie` to prepare. + get_storage_root : + Function to get the storage root of an account. Needed to encode + `Account` objects. + + Returns + ------- + out : `Mapping[ethereum.base_types.Bytes, Node]` + Object with keys mapped to nibble-byte form. + """ + mapped: MutableMapping[Bytes, Bytes] = {} + + for preimage, value in trie._data.items(): + if isinstance(value, Account): + assert get_storage_root is not None + address = Address(preimage) + encoded_value = encode_node(value, get_storage_root(address)) + else: + encoded_value = encode_node(value) + if encoded_value == b"": + raise AssertionError + key: Bytes + if trie.secured: + # "secure" tries hash keys once before construction + key = keccak256(preimage) + else: + key = preimage + mapped[bytes_to_nibble_list(key)] = encoded_value + + return mapped + + +def root( + trie: Trie[K, V], + get_storage_root: Optional[Callable[[Address], Root]] = None, +) -> Root: + """ + Computes the root of a modified merkle patricia trie (MPT). + + Parameters + ---------- + trie : + `Trie` to get the root of. + get_storage_root : + Function to get the storage root of an account. Needed to encode + `Account` objects. + + + Returns + ------- + root : `.fork_types.Root` + MPT root of the underlying key-value pairs. + """ + obj = _prepare_trie(trie, get_storage_root) + + root_node = encode_internal_node(patricialize(obj, Uint(0))) + if len(rlp.encode(root_node)) < 32: + return keccak256(rlp.encode(root_node)) + else: + assert isinstance(root_node, Bytes) + return Root(root_node) + + +def patricialize( + obj: Mapping[Bytes, Bytes], level: Uint +) -> Optional[InternalNode]: + """ + Structural composition function. + + Used to recursively patricialize and merkleize a dictionary. Includes + memoization of the tree structure and hashes. + + Parameters + ---------- + obj : + Underlying trie key-value pairs, with keys in nibble-list format. + level : + Current trie level. + + Returns + ------- + node : `ethereum.base_types.Bytes` + Root node of `obj`. + """ + if len(obj) == 0: + return None + + arbitrary_key = next(iter(obj)) + + # if leaf node + if len(obj) == 1: + leaf = LeafNode(arbitrary_key[level:], obj[arbitrary_key]) + return leaf + + # prepare for extension node check by finding max j such that all keys in + # obj have the same key[i:j] + substring = arbitrary_key[level:] + prefix_length = len(substring) + for key in obj: + prefix_length = min( + prefix_length, common_prefix_length(substring, key[level:]) + ) + + # finished searching, found another key at the current level + if prefix_length == 0: + break + + # if extension node + if prefix_length > 0: + prefix = arbitrary_key[int(level) : int(level) + prefix_length] + return ExtensionNode( + prefix, + encode_internal_node( + patricialize(obj, level + Uint(prefix_length)) + ), + ) + + branches: List[MutableMapping[Bytes, Bytes]] = [] + for _ in range(16): + branches.append({}) + value = b"" + for key in obj: + if len(key) == level: + # shouldn't ever have an account or receipt in an internal node + if isinstance(obj[key], (Account, Receipt, Uint)): + raise AssertionError + value = obj[key] + else: + branches[key[level]][key] = obj[key] + + subnodes = tuple( + encode_internal_node(patricialize(branches[k], level + Uint(1))) + for k in range(16) + ) + return BranchNode( + cast(BranchSubnodes, assert_type(subnodes, Tuple[Extended, ...])), + value, + ) diff --git a/src/ethereum/forks/amsterdam/utils/__init__.py b/src/ethereum/forks/amsterdam/utils/__init__.py new file mode 100644 index 0000000000..224a4d269b --- /dev/null +++ b/src/ethereum/forks/amsterdam/utils/__init__.py @@ -0,0 +1,3 @@ +""" +Utility functions unique to this particular fork. +""" diff --git a/src/ethereum/forks/amsterdam/utils/address.py b/src/ethereum/forks/amsterdam/utils/address.py new file mode 100644 index 0000000000..69f91d0a03 --- /dev/null +++ b/src/ethereum/forks/amsterdam/utils/address.py @@ -0,0 +1,91 @@ +""" +Hardfork Utility Functions For Addresses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Address specific functions used in this amsterdam 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.utils.byte import left_pad_zero_bytes + +from ..fork_types import Address + + +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.amsterdam.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/amsterdam/utils/hexadecimal.py b/src/ethereum/forks/amsterdam/utils/hexadecimal.py new file mode 100644 index 0000000000..0246de9cc1 --- /dev/null +++ b/src/ethereum/forks/amsterdam/utils/hexadecimal.py @@ -0,0 +1,53 @@ +""" +Utility Functions For Hexadecimal Strings +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Hexadecimal utility functions used in this specification, specific to +Amsterdam types. +""" +from ethereum_types.bytes import Bytes + +from ethereum.utils.hexadecimal import remove_hex_prefix + +from ..fork_types import Address, Root + + +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/amsterdam/utils/message.py b/src/ethereum/forks/amsterdam/utils/message.py new file mode 100644 index 0000000000..4d4ba5ade6 --- /dev/null +++ b/src/ethereum/forks/amsterdam/utils/message.py @@ -0,0 +1,90 @@ +""" +Hardfork Utility Functions For The Message Data-structure +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Message specific functions used in this amsterdam version of +specification. +""" + +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import Uint + +from ..fork_types import Address +from ..state import get_account +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.amsterdam.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(block_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_account(block_env.state, tx.to).code + 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, + 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/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py new file mode 100644 index 0000000000..033293a5fd --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -0,0 +1,190 @@ +""" +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 + +from ethereum_types.bytes import Bytes, Bytes0, Bytes32 +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.crypto.hash import Hash32 +from ethereum.exceptions import EthereumException + +from ..blocks import Log, Receipt, Withdrawal +from ..fork_types import Address, Authorization, VersionedHash +from ..state import State, TransientStorage +from ..transactions import LegacyTransaction +from ..trie import Trie + +__all__ = ("Environment", "Evm", "Message") + + +@dataclass +class BlockEnvironment: + """ + Items external to the virtual machine itself, provided by the environment. + """ + + chain_id: U64 + state: State + 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 + + +@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. + 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 : + Key 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_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) + + +@dataclass +class TransactionEnvironment: + """ + Items that are used by contract creation or message call. + """ + + origin: Address + gas_price: Uint + gas: Uint + access_list_addresses: Set[Address] + access_list_storage_keys: Set[Tuple[Address, Bytes32]] + transient_storage: TransientStorage + blob_versioned_hashes: Tuple[VersionedHash, ...] + authorizations: Tuple[Authorization, ...] + index_in_block: Optional[Uint] + tx_hash: Optional[Hash32] + + +@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 + 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"] + + +@dataclass +class Evm: + """The internal state of the virtual machine.""" + + pc: Uint + stack: List[U256] + memory: bytearray + code: Bytes + 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]] + + +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.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) + + +def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: + """ + Incorporate the state of an unsuccessful `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 diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py new file mode 100644 index 0000000000..1fe2e1e7bd --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -0,0 +1,207 @@ +""" +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 ..fork_types import Address, Authorization +from ..state import account_exists, get_account, increment_nonce, set_code +from ..utils.hexadecimal import hex_to_address +from ..vm.gas import GAS_COLD_ACCOUNT_ACCESS, GAS_WARM_ACCESS +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 +PER_EMPTY_ACCOUNT_COST = 25000 +PER_AUTH_BASE_COST = 12500 +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. + """ + if ( + len(code) == EOA_DELEGATED_CODE_LENGTH + and code[:EOA_DELEGATION_MARKER_LENGTH] == EOA_DELEGATION_MARKER + ): + return True + return False + + +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 access_delegation( + evm: Evm, address: Address +) -> Tuple[bool, Address, Bytes, Uint]: + """ + Get the delegation address, code, and the cost of access from the address. + + Parameters + ---------- + evm : `Evm` + The execution frame. + address : `Address` + The address to get the delegation from. + + Returns + ------- + delegation : `Tuple[bool, Address, Bytes, Uint]` + The delegation address, code, and access gas cost. + """ + state = evm.message.block_env.state + code = get_account(state, address).code + if not is_valid_delegation(code): + return False, address, code, Uint(0) + + address = Address(code[EOA_DELEGATION_MARKER_LENGTH:]) + if address in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + code = get_account(state, address).code + + return True, address, code, access_gas_cost + + +def set_delegation(message: Message) -> U256: + """ + Set the delegation code for the authorities in the message. + + Parameters + ---------- + message : + Transaction specific items. + env : + External items required for EVM execution. + + Returns + ------- + refund_counter: `U256` + Refund from authority which already exists in state. + """ + state = message.block_env.state + refund_counter = U256(0) + for auth in message.tx_env.authorizations: + if auth.chain_id not in (message.block_env.chain_id, U256(0)): + continue + + if auth.nonce >= U64.MAX_VALUE: + continue + + try: + authority = recover_authority(auth) + except InvalidSignatureError: + continue + + message.accessed_addresses.add(authority) + + authority_account = get_account(state, authority) + authority_code = authority_account.code + + if authority_code and not is_valid_delegation(authority_code): + continue + + authority_nonce = authority_account.nonce + if authority_nonce != auth.nonce: + continue + + if account_exists(state, authority): + refund_counter += U256(PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST) + + if auth.address == NULL_ADDRESS: + code_to_set = b"" + else: + code_to_set = EOA_DELEGATION_MARKER + auth.address + set_code(state, authority, code_to_set) + + increment_nonce(state, authority) + + if message.code_address is None: + raise InvalidBlock("Invalid type 4 transaction: no target") + + message.code = get_account(state, message.code_address).code + + return refund_counter diff --git a/src/ethereum/forks/amsterdam/vm/exceptions.py b/src/ethereum/forks/amsterdam/vm/exceptions.py new file mode 100644 index 0000000000..2a4f2d2f65 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/exceptions.py @@ -0,0 +1,140 @@ +""" +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/amsterdam/vm/gas.py b/src/ethereum/forks/amsterdam/vm/gas.py new file mode 100644 index 0000000000..e570fac5fc --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/gas.py @@ -0,0 +1,387 @@ +""" +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 List, Tuple + +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.numeric import ceil32, taylor_exponential + +from ..blocks import Header +from ..transactions import BlobTransaction, Transaction +from . import Evm +from .exceptions import OutOfGasError + +GAS_JUMPDEST = Uint(1) +GAS_BASE = Uint(2) +GAS_VERY_LOW = Uint(3) +GAS_STORAGE_SET = Uint(20000) +GAS_STORAGE_UPDATE = Uint(5000) +GAS_STORAGE_CLEAR_REFUND = Uint(4800) +GAS_LOW = Uint(5) +GAS_MID = Uint(8) +GAS_HIGH = Uint(10) +GAS_EXPONENTIATION = Uint(10) +GAS_EXPONENTIATION_PER_BYTE = Uint(50) +GAS_MEMORY = Uint(3) +GAS_KECCAK256 = Uint(30) +GAS_KECCAK256_WORD = Uint(6) +GAS_COPY = Uint(3) +GAS_BLOCK_HASH = Uint(20) +GAS_LOG = Uint(375) +GAS_LOG_DATA = Uint(8) +GAS_LOG_TOPIC = Uint(375) +GAS_CREATE = Uint(32000) +GAS_CODE_DEPOSIT = Uint(200) +GAS_ZERO = Uint(0) +GAS_NEW_ACCOUNT = Uint(25000) +GAS_CALL_VALUE = Uint(9000) +GAS_CALL_STIPEND = Uint(2300) +GAS_SELF_DESTRUCT = Uint(5000) +GAS_SELF_DESTRUCT_NEW_ACCOUNT = Uint(25000) +GAS_ECRECOVER = Uint(3000) +GAS_P256VERIFY = Uint(6900) +GAS_SHA256 = Uint(60) +GAS_SHA256_WORD = Uint(12) +GAS_RIPEMD160 = Uint(600) +GAS_RIPEMD160_WORD = Uint(120) +GAS_IDENTITY = Uint(15) +GAS_IDENTITY_WORD = Uint(3) +GAS_RETURN_DATA_COPY = Uint(3) +GAS_FAST_STEP = Uint(5) +GAS_BLAKE2_PER_ROUND = Uint(1) +GAS_COLD_SLOAD = Uint(2100) +GAS_COLD_ACCOUNT_ACCESS = Uint(2600) +GAS_WARM_ACCESS = Uint(100) +GAS_INIT_CODE_WORD_COST = Uint(2) +GAS_BLOBHASH_OPCODE = Uint(3) +GAS_POINT_EVALUATION = Uint(50000) + +GAS_PER_BLOB = U64(2**17) +BLOB_SCHEDULE_TARGET = U64(6) +TARGET_BLOB_GAS_PER_BLOCK = GAS_PER_BLOB * BLOB_SCHEDULE_TARGET +BLOB_BASE_COST = Uint(2**13) +BLOB_SCHEDULE_MAX = U64(9) +MIN_BLOB_GASPRICE = Uint(1) +BLOB_BASE_FEE_UPDATE_FRACTION = Uint(5007716) + +GAS_BLS_G1_ADD = Uint(375) +GAS_BLS_G1_MUL = Uint(12000) +GAS_BLS_G1_MAP = Uint(5500) +GAS_BLS_G2_ADD = Uint(600) +GAS_BLS_G2_MUL = Uint(22500) +GAS_BLS_G2_MAP = Uint(23800) + + +@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 + + +@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 charge_gas(evm: Evm, amount: Uint) -> None: + """ + Subtracts `amount` from `evm.gas_left`. + + Parameters + ---------- + evm : + The current EVM. + amount : + The amount of gas the current operation requires. + + """ + evm_trace(evm, GasAndRefund(int(amount))) + + if evm.gas_left < amount: + raise OutOfGasError + else: + evm.gas_left -= amount + + +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 * GAS_MEMORY + 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 = Uint(len(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 = GAS_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 GAS_INIT_CODE_WORD_COST * ceil32(init_code_length) // Uint(32) + + +def calculate_excess_blob_gas(parent_header: Header) -> U64: + """ + Calculated 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 < TARGET_BLOB_GAS_PER_BLOCK: + return U64(0) + + target_blob_gas_price = Uint(GAS_PER_BLOB) + target_blob_gas_price *= calculate_blob_gas_price(excess_blob_gas) + + base_blob_tx_price = BLOB_BASE_COST * base_fee_per_gas + if base_blob_tx_price > target_blob_gas_price: + blob_schedule_delta = BLOB_SCHEDULE_MAX - BLOB_SCHEDULE_TARGET + return ( + excess_blob_gas + + blob_gas_used * blob_schedule_delta // BLOB_SCHEDULE_MAX + ) + + return parent_blob_gas - TARGET_BLOB_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 GAS_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( + MIN_BLOB_GASPRICE, + Uint(excess_blob_gas), + 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/amsterdam/vm/instructions/__init__.py b/src/ethereum/forks/amsterdam/vm/instructions/__init__.py new file mode 100644 index 0000000000..9cc30668e7 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/__init__.py @@ -0,0 +1,368 @@ +""" +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 + + # 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 + + # 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.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.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/amsterdam/vm/instructions/arithmetic.py b/src/ethereum/forks/amsterdam/vm/instructions/arithmetic.py new file mode 100644 index 0000000000..28c97db189 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/arithmetic.py @@ -0,0 +1,374 @@ +""" +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 ( + GAS_EXPONENTIATION, + GAS_EXPONENTIATION_PER_BYTE, + GAS_LOW, + GAS_MID, + GAS_VERY_LOW, + 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, GAS_VERY_LOW) + + # 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, GAS_VERY_LOW) + + # OPERATION + result = x.wrapping_sub(y) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mul(evm: Evm) -> None: + """ + Multiply 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, GAS_LOW) + + # 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, GAS_LOW) + + # 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, GAS_LOW) + + # 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, GAS_LOW) + + # 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, GAS_LOW) + + # 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, GAS_MID) + + # 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, GAS_MID) + + # 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, GAS_EXPONENTIATION + GAS_EXPONENTIATION_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, GAS_LOW) + + # OPERATION + if byte_num > U256(31): + # Can't extend any further + result = value + else: + # U256(0).to_be_bytes() gives b'' instead 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/amsterdam/vm/instructions/bitwise.py b/src/ethereum/forks/amsterdam/vm/instructions/bitwise.py new file mode 100644 index 0000000000..cc96b9da0e --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/bitwise.py @@ -0,0 +1,268 @@ +""" +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 GAS_LOW, GAS_VERY_LOW, 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, GAS_VERY_LOW) + + # 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, GAS_VERY_LOW) + + # 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, GAS_VERY_LOW) + + # 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, GAS_VERY_LOW) + + # 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, GAS_VERY_LOW) + + # 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, GAS_VERY_LOW) + + # 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, GAS_VERY_LOW) + + # 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, GAS_VERY_LOW) + + # 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, GAS_LOW) + + # 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/amsterdam/vm/instructions/block.py b/src/ethereum/forks/amsterdam/vm/instructions/block.py new file mode 100644 index 0000000000..51ddc69f85 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/block.py @@ -0,0 +1,255 @@ +""" +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 GAS_BASE, GAS_BLOCK_HASH, 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.amsterdam.vm.exceptions.StackUnderflowError` + If `len(stack)` is less than `1`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `20`. + """ + # STACK + block_number = Uint(pop(evm.stack)) + + # GAS + charge_gas(evm, GAS_BLOCK_HASH) + + # 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.amsterdam.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # 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 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.amsterdam.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # 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.amsterdam.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # 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.amsterdam.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # 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.amsterdam.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # 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.amsterdam.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.amsterdam.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.chain_id)) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/comparison.py b/src/ethereum/forks/amsterdam/vm/instructions/comparison.py new file mode 100644 index 0000000000..275455ba53 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/comparison.py @@ -0,0 +1,178 @@ +""" +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 GAS_VERY_LOW, 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, GAS_VERY_LOW) + + # 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, GAS_VERY_LOW) + + # 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, GAS_VERY_LOW) + + # 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, GAS_VERY_LOW) + + # 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, GAS_VERY_LOW) + + # 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, GAS_VERY_LOW) + + # OPERATION + result = U256(x == 0) + + push(evm.stack, result) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/control_flow.py b/src/ethereum/forks/amsterdam/vm/instructions/control_flow.py new file mode 100644 index 0000000000..7722661f79 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/control_flow.py @@ -0,0 +1,171 @@ +""" +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 GAS_BASE, GAS_HIGH, GAS_JUMPDEST, GAS_MID, 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, GAS_MID) + + # 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, GAS_HIGH) + + # 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, GAS_BASE) + + # 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, GAS_BASE) + + # 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, GAS_JUMPDEST) + + # OPERATION + pass + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/environment.py b/src/ethereum/forks/amsterdam/vm/instructions/environment.py new file mode 100644 index 0000000000..226b3d3bb3 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/environment.py @@ -0,0 +1,597 @@ +""" +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.crypto.hash import keccak256 +from ethereum.utils.numeric import ceil32 + +from ...fork_types import EMPTY_ACCOUNT +from ...state import get_account +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 ( + GAS_BASE, + GAS_BLOBHASH_OPCODE, + GAS_COLD_ACCOUNT_ACCESS, + GAS_COPY, + GAS_FAST_STEP, + GAS_RETURN_DATA_COPY, + GAS_VERY_LOW, + GAS_WARM_ACCESS, + 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, GAS_BASE) + + # 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, GAS_WARM_ACCESS) + else: + evm.accessed_addresses.add(address) + charge_gas(evm, GAS_COLD_ACCOUNT_ACCESS) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. + balance = get_account(evm.message.block_env.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, GAS_BASE) + + # 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, GAS_BASE) + + # 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, GAS_BASE) + + # 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, GAS_VERY_LOW) + + # 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, GAS_BASE) + + # 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 = GAS_COPY * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas(evm, GAS_VERY_LOW + 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, GAS_BASE) + + # 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 = GAS_COPY * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas(evm, GAS_VERY_LOW + 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, GAS_BASE) + + # 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 = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) + + # OPERATION + code = get_account(evm.message.block_env.state, address).code + + 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 = GAS_COPY * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + + if address in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost + copy_gas_cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + code = get_account(evm.message.block_env.state, address).code + + 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, GAS_BASE) + + # 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 code 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 = GAS_RETURN_DATA_COPY * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas(evm, GAS_VERY_LOW + 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 = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(address) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + charge_gas(evm, access_gas_cost) + + # OPERATION + account = get_account(evm.message.block_env.state, address) + + if account == EMPTY_ACCOUNT: + codehash = U256(0) + else: + code = account.code + codehash = U256.from_be_bytes(keccak256(code)) + + 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, GAS_FAST_STEP) + + # OPERATION + # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. + balance = get_account( + evm.message.block_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, GAS_BASE) + + # 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, GAS_BLOBHASH_OPCODE) + + # 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, GAS_BASE) + + # 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/amsterdam/vm/instructions/keccak.py b/src/ethereum/forks/amsterdam/vm/instructions/keccak.py new file mode 100644 index 0000000000..06f6b65070 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/keccak.py @@ -0,0 +1,64 @@ +""" +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 ( + GAS_KECCAK256, + GAS_KECCAK256_WORD, + 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 = GAS_KECCAK256_WORD * words + extend_memory = calculate_gas_extend_memory( + evm.memory, [(memory_start_index, size)] + ) + charge_gas(evm, GAS_KECCAK256 + 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/amsterdam/vm/instructions/log.py b/src/ethereum/forks/amsterdam/vm/instructions/log.py new file mode 100644 index 0000000000..87c06ed6be --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/log.py @@ -0,0 +1,88 @@ +""" +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 ethereum_types.numeric import Uint + +from ...blocks import Log +from .. import Evm +from ..exceptions import WriteInStaticContext +from ..gas import ( + GAS_LOG, + GAS_LOG_DATA, + GAS_LOG_TOPIC, + 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, + GAS_LOG + + GAS_LOG_DATA * Uint(size) + + GAS_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 = partial(log_n, num_topics=0) +log1 = partial(log_n, num_topics=1) +log2 = partial(log_n, num_topics=2) +log3 = partial(log_n, num_topics=3) +log4 = partial(log_n, num_topics=4) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/memory.py b/src/ethereum/forks/amsterdam/vm/instructions/memory.py new file mode 100644 index 0000000000..89533af37e --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/memory.py @@ -0,0 +1,177 @@ +""" +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 ( + GAS_BASE, + GAS_COPY, + GAS_VERY_LOW, + 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, GAS_VERY_LOW + 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, GAS_VERY_LOW + 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: + """ + Load 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, GAS_VERY_LOW + 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: + """ + Push the size of active memory in bytes onto the stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + push(evm.stack, U256(len(evm.memory))) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def mcopy(evm: Evm) -> None: + """ + Copy 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 = GAS_COPY * words + + extend_memory = calculate_gas_extend_memory( + evm.memory, [(source, length), (destination, length)] + ) + charge_gas(evm, GAS_VERY_LOW + 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/amsterdam/vm/instructions/stack.py b/src/ethereum/forks/amsterdam/vm/instructions/stack.py new file mode 100644 index 0000000000..2e8a492412 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/stack.py @@ -0,0 +1,209 @@ +""" +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 ethereum_types.numeric import U256, Uint + +from .. import Evm, stack +from ..exceptions import StackUnderflowError +from ..gas import GAS_BASE, GAS_VERY_LOW, charge_gas +from ..memory import buffer_read + + +def pop(evm: Evm) -> None: + """ + Remove item from stack. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + stack.pop(evm.stack) + + # GAS + charge_gas(evm, GAS_BASE) + + # OPERATION + pass + + # PROGRAM COUNTER + evm.pc += Uint(1) + + +def push_n(evm: Evm, num_bytes: int) -> None: + """ + Pushes a 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, GAS_BASE) + else: + charge_gas(evm, GAS_VERY_LOW) + + # 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: + """ + Duplicate 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, GAS_VERY_LOW) + 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: + """ + Swap 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, GAS_VERY_LOW) + 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 = partial(push_n, num_bytes=0) +push1 = partial(push_n, num_bytes=1) +push2 = partial(push_n, num_bytes=2) +push3 = partial(push_n, num_bytes=3) +push4 = partial(push_n, num_bytes=4) +push5 = partial(push_n, num_bytes=5) +push6 = partial(push_n, num_bytes=6) +push7 = partial(push_n, num_bytes=7) +push8 = partial(push_n, num_bytes=8) +push9 = partial(push_n, num_bytes=9) +push10 = partial(push_n, num_bytes=10) +push11 = partial(push_n, num_bytes=11) +push12 = partial(push_n, num_bytes=12) +push13 = partial(push_n, num_bytes=13) +push14 = partial(push_n, num_bytes=14) +push15 = partial(push_n, num_bytes=15) +push16 = partial(push_n, num_bytes=16) +push17 = partial(push_n, num_bytes=17) +push18 = partial(push_n, num_bytes=18) +push19 = partial(push_n, num_bytes=19) +push20 = partial(push_n, num_bytes=20) +push21 = partial(push_n, num_bytes=21) +push22 = partial(push_n, num_bytes=22) +push23 = partial(push_n, num_bytes=23) +push24 = partial(push_n, num_bytes=24) +push25 = partial(push_n, num_bytes=25) +push26 = partial(push_n, num_bytes=26) +push27 = partial(push_n, num_bytes=27) +push28 = partial(push_n, num_bytes=28) +push29 = partial(push_n, num_bytes=29) +push30 = partial(push_n, num_bytes=30) +push31 = partial(push_n, num_bytes=31) +push32 = partial(push_n, num_bytes=32) + +dup1 = partial(dup_n, item_number=0) +dup2 = partial(dup_n, item_number=1) +dup3 = partial(dup_n, item_number=2) +dup4 = partial(dup_n, item_number=3) +dup5 = partial(dup_n, item_number=4) +dup6 = partial(dup_n, item_number=5) +dup7 = partial(dup_n, item_number=6) +dup8 = partial(dup_n, item_number=7) +dup9 = partial(dup_n, item_number=8) +dup10 = partial(dup_n, item_number=9) +dup11 = partial(dup_n, item_number=10) +dup12 = partial(dup_n, item_number=11) +dup13 = partial(dup_n, item_number=12) +dup14 = partial(dup_n, item_number=13) +dup15 = partial(dup_n, item_number=14) +dup16 = partial(dup_n, item_number=15) + +swap1 = partial(swap_n, item_number=1) +swap2 = partial(swap_n, item_number=2) +swap3 = partial(swap_n, item_number=3) +swap4 = partial(swap_n, item_number=4) +swap5 = partial(swap_n, item_number=5) +swap6 = partial(swap_n, item_number=6) +swap7 = partial(swap_n, item_number=7) +swap8 = partial(swap_n, item_number=8) +swap9 = partial(swap_n, item_number=9) +swap10 = partial(swap_n, item_number=10) +swap11 = partial(swap_n, item_number=11) +swap12 = partial(swap_n, item_number=12) +swap13 = partial(swap_n, item_number=13) +swap14 = partial(swap_n, item_number=14) +swap15 = partial(swap_n, item_number=15) +swap16 = partial(swap_n, item_number=16) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py new file mode 100644 index 0000000000..65a0d5a9b6 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -0,0 +1,184 @@ +""" +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 ...state import ( + get_storage, + get_storage_original, + get_transient_storage, + set_storage, + set_transient_storage, +) +from .. import Evm +from ..exceptions import OutOfGasError, WriteInStaticContext +from ..gas import ( + GAS_CALL_STIPEND, + GAS_COLD_SLOAD, + GAS_STORAGE_CLEAR_REFUND, + GAS_STORAGE_SET, + GAS_STORAGE_UPDATE, + GAS_WARM_ACCESS, + charge_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, GAS_WARM_ACCESS) + else: + evm.accessed_storage_keys.add((evm.message.current_target, key)) + charge_gas(evm, GAS_COLD_SLOAD) + + # OPERATION + value = get_storage( + evm.message.block_env.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. + + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + new_value = pop(evm.stack) + if evm.gas_left <= GAS_CALL_STIPEND: + raise OutOfGasError + + state = evm.message.block_env.state + original_value = get_storage_original( + state, evm.message.current_target, key + ) + current_value = get_storage(state, evm.message.current_target, key) + + gas_cost = Uint(0) + + if (evm.message.current_target, key) not in evm.accessed_storage_keys: + evm.accessed_storage_keys.add((evm.message.current_target, key)) + gas_cost += GAS_COLD_SLOAD + + if original_value == current_value and current_value != new_value: + if original_value == 0: + gas_cost += GAS_STORAGE_SET + else: + gas_cost += GAS_STORAGE_UPDATE - GAS_COLD_SLOAD + else: + gas_cost += GAS_WARM_ACCESS + + # 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 += int(GAS_STORAGE_CLEAR_REFUND) + + if original_value != 0 and current_value == 0: + # Gas refund issued earlier to be reversed + evm.refund_counter -= int(GAS_STORAGE_CLEAR_REFUND) + + if original_value == new_value: + # Storage slot being restored to its original value + if original_value == 0: + # Slot was originally empty and was SET earlier + evm.refund_counter += int(GAS_STORAGE_SET - GAS_WARM_ACCESS) + else: + # Slot was originally non-empty and was UPDATED earlier + evm.refund_counter += int( + GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS + ) + + charge_gas(evm, gas_cost) + if evm.message.is_static: + raise WriteInStaticContext + set_storage(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, GAS_WARM_ACCESS) + + # OPERATION + value = get_transient_storage( + evm.message.tx_env.transient_storage, 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. + """ + # STACK + key = pop(evm.stack).to_be_bytes32() + new_value = pop(evm.stack) + + # GAS + charge_gas(evm, GAS_WARM_ACCESS) + if evm.message.is_static: + raise WriteInStaticContext + set_transient_storage( + evm.message.tx_env.transient_storage, + evm.message.current_target, + key, + new_value, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py new file mode 100644 index 0000000000..d7308821bd --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -0,0 +1,742 @@ +""" +Ethereum Virtual Machine (EVM) System Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementations of the EVM system related instructions. +""" + +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import U256, Uint + +from ethereum.utils.numeric import ceil32 + +from ...fork_types import Address +from ...state import ( + account_has_code_or_nonce, + account_has_storage, + get_account, + increment_nonce, + is_account_alive, + move_ether, + set_account_balance, +) +from ...utils.address import ( + compute_contract_address, + compute_create2_contract_address, + to_address_masked, +) +from ...vm.eoa_delegation import access_delegation +from .. import ( + Evm, + Message, + incorporate_child_on_error, + incorporate_child_on_success, +) +from ..exceptions import OutOfGasError, Revert, WriteInStaticContext +from ..gas import ( + GAS_CALL_VALUE, + GAS_COLD_ACCOUNT_ACCESS, + GAS_CREATE, + GAS_KECCAK256_WORD, + GAS_NEW_ACCOUNT, + GAS_SELF_DESTRUCT, + GAS_SELF_DESTRUCT_NEW_ACCOUNT, + GAS_WARM_ACCESS, + GAS_ZERO, + calculate_gas_extend_memory, + calculate_message_call_gas, + charge_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, + ) + + call_data = memory_read_bytes( + evm.memory, memory_start_position, memory_size + ) + if len(call_data) > MAX_INIT_CODE_SIZE: + raise OutOfGasError + + create_message_gas = max_message_call_gas(Uint(evm.gas_left)) + evm.gas_left -= create_message_gas + if evm.message.is_static: + raise WriteInStaticContext + evm.return_data = b"" + + sender_address = evm.message.current_target + sender = get_account(evm.message.block_env.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 + push(evm.stack, U256(0)) + return + + evm.accessed_addresses.add(contract_address) + + if account_has_code_or_nonce( + evm.message.block_env.state, contract_address + ) or account_has_storage(evm.message.block_env.state, contract_address): + increment_nonce( + evm.message.block_env.state, evm.message.current_target + ) + push(evm.stack, U256(0)) + return + + increment_nonce(evm.message.block_env.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, + 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) + evm.return_data = child_evm.output + push(evm.stack, U256(0)) + else: + incorporate_child_on_success(evm, child_evm) + 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. + """ + # 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, GAS_CREATE + 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.block_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 CREATE opcode except that the address of new account + depends on the init_code instead of the nonce of sender. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # 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, + GAS_CREATE + + GAS_KECCAK256_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, GAS_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 + + +def generic_call( + evm: Evm, + gas: 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, +) -> 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 += gas + push(evm.stack, U256(0)) + return + + call_data = memory_read_bytes( + evm.memory, memory_input_start_position, memory_input_size + ) + + child_message = Message( + block_env=evm.message.block_env, + tx_env=evm.message.tx_env, + caller=caller, + target=to, + gas=gas, + value=value, + data=call_data, + code=code, + current_target=to, + depth=evm.message.depth + Uint(1), + code_address=code_address, + should_transfer_value=should_transfer_value, + is_static=True if is_staticcall else evm.message.is_static, + accessed_addresses=evm.accessed_addresses.copy(), + accessed_storage_keys=evm.accessed_storage_keys.copy(), + disable_precompiles=disable_precompiles, + parent_evm=evm, + ) + child_evm = process_message(child_message) + + if child_evm.error: + incorporate_child_on_error(evm, child_evm) + 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, U256(1)) + + actual_output_size = min(memory_output_size, U256(len(child_evm.output))) + memory_write( + evm.memory, + 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) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, + [ + (memory_input_start_position, memory_input_size), + (memory_output_start_position, memory_output_size), + ], + ) + + if to in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(to) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + code_address = to + ( + disable_precompiles, + code_address, + code, + delegated_access_gas_cost, + ) = access_delegation(evm, code_address) + access_gas_cost += delegated_access_gas_cost + + create_gas_cost = GAS_NEW_ACCOUNT + if value == 0 or is_account_alive(evm.message.block_env.state, to): + create_gas_cost = Uint(0) + transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + create_gas_cost + transfer_gas_cost, + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + if evm.message.is_static and value != U256(0): + raise WriteInStaticContext + evm.memory += b"\x00" * extend_memory.expand_by + sender_balance = get_account( + evm.message.block_env.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 + else: + generic_call( + evm, + message_call_gas.sub_call, + value, + evm.message.current_target, + to, + code_address, + True, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + ) + + # 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), + ], + ) + + if code_address in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(code_address) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + ( + disable_precompiles, + code_address, + code, + delegated_access_gas_cost, + ) = access_delegation(evm, code_address) + access_gas_cost += delegated_access_gas_cost + + transfer_gas_cost = Uint(0) if value == 0 else GAS_CALL_VALUE + message_call_gas = calculate_message_call_gas( + value, + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost + transfer_gas_cost, + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + sender_balance = get_account( + evm.message.block_env.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 + else: + generic_call( + evm, + message_call_gas.sub_call, + value, + evm.message.current_target, + to, + code_address, + True, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + ) + + # 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. + """ + # STACK + beneficiary = to_address_masked(pop(evm.stack)) + + # GAS + gas_cost = GAS_SELF_DESTRUCT + if beneficiary not in evm.accessed_addresses: + evm.accessed_addresses.add(beneficiary) + gas_cost += GAS_COLD_ACCOUNT_ACCESS + + if ( + not is_account_alive(evm.message.block_env.state, beneficiary) + and get_account( + evm.message.block_env.state, evm.message.current_target + ).balance + != 0 + ): + gas_cost += GAS_SELF_DESTRUCT_NEW_ACCOUNT + + charge_gas(evm, gas_cost) + if evm.message.is_static: + raise WriteInStaticContext + + originator = evm.message.current_target + originator_balance = get_account( + evm.message.block_env.state, originator + ).balance + + move_ether( + evm.message.block_env.state, + originator, + beneficiary, + originator_balance, + ) + + # register account for deletion only if it was created + # in the same transaction + if originator in evm.message.block_env.state.created_accounts: + # If beneficiary is the same as originator, then + # the ether is burnt. + set_account_balance(evm.message.block_env.state, originator, U256(0)) + 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), + ], + ) + + if code_address in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(code_address) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + ( + disable_precompiles, + code_address, + code, + delegated_access_gas_cost, + ) = access_delegation(evm, code_address) + access_gas_cost += delegated_access_gas_cost + + message_call_gas = calculate_message_call_gas( + U256(0), gas, Uint(evm.gas_left), extend_memory.cost, access_gas_cost + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + generic_call( + evm, + message_call_gas.sub_call, + evm.message.value, + evm.message.caller, + evm.message.current_target, + code_address, + False, + False, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + ) + + # 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), + ], + ) + + if to in evm.accessed_addresses: + access_gas_cost = GAS_WARM_ACCESS + else: + evm.accessed_addresses.add(to) + access_gas_cost = GAS_COLD_ACCOUNT_ACCESS + + code_address = to + ( + disable_precompiles, + code_address, + code, + delegated_access_gas_cost, + ) = access_delegation(evm, code_address) + access_gas_cost += delegated_access_gas_cost + + message_call_gas = calculate_message_call_gas( + U256(0), + gas, + Uint(evm.gas_left), + extend_memory.cost, + access_gas_cost, + ) + charge_gas(evm, message_call_gas.cost + extend_memory.cost) + + # OPERATION + evm.memory += b"\x00" * extend_memory.expand_by + generic_call( + evm, + message_call_gas.sub_call, + U256(0), + evm.message.current_target, + to, + code_address, + True, + True, + memory_input_start_position, + memory_input_size, + memory_output_start_position, + memory_output_size, + code, + disable_precompiles, + ) + + # 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/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py new file mode 100644 index 0000000000..fb893aaa6b --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -0,0 +1,321 @@ +""" +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 + +from ethereum_types.bytes import Bytes, Bytes0 +from ethereum_types.numeric import U256, Uint, ulen + +from ethereum.exceptions import EthereumException +from ethereum.trace import ( + EvmStop, + OpEnd, + OpException, + OpStart, + PrecompileEnd, + PrecompileStart, + TransactionEnd, + evm_trace, +) + +from ..blocks import Log +from ..fork_types import Address +from ..state import ( + account_has_code_or_nonce, + account_has_storage, + begin_transaction, + commit_transaction, + destroy_storage, + get_account, + increment_nonce, + mark_account_created, + move_ether, + rollback_transaction, + set_code, +) +from ..vm import Message +from ..vm.eoa_delegation import get_delegated_code_address, set_delegation +from ..vm.gas import GAS_CODE_DEPOSIT, charge_gas +from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS +from . import Evm +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 = 0x6000 +MAX_INIT_CODE_SIZE = 2 * MAX_CODE_SIZE + + +@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. + """ + + gas_left: Uint + refund_counter: U256 + logs: Tuple[Log, ...] + accounts_to_delete: Set[Address] + error: Optional[EthereumException] + return_data: Bytes + + +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 + """ + block_env = message.block_env + refund_counter = U256(0) + if message.target == Bytes0(b""): + is_collision = account_has_code_or_nonce( + block_env.state, message.current_target + ) or account_has_storage(block_env.state, message.current_target) + if is_collision: + return MessageCallOutput( + Uint(0), + U256(0), + tuple(), + set(), + AddressCollision(), + Bytes(b""), + ) + else: + evm = process_create_message(message) + else: + if message.tx_env.authorizations != (): + refund_counter += set_delegation(message) + + delegated_address = get_delegated_code_address(message.code) + if delegated_address is not None: + message.disable_precompiles = True + message.accessed_addresses.add(delegated_address) + message.code = get_account(block_env.state, delegated_address).code + 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, + ) + + +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.amsterdam.vm.Evm` + Items containing execution specific objects. + """ + state = message.block_env.state + transient_storage = message.tx_env.transient_storage + # take snapshot of state before processing the message + begin_transaction(state, transient_storage) + + # 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(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(state, message.current_target) + + increment_nonce(state, message.current_target) + evm = process_message(message) + if not evm.error: + contract_code = evm.output + contract_code_gas = Uint(len(contract_code)) * GAS_CODE_DEPOSIT + try: + if len(contract_code) > 0: + if contract_code[0] == 0xEF: + raise InvalidContractPrefix + charge_gas(evm, contract_code_gas) + if len(contract_code) > MAX_CODE_SIZE: + raise OutOfGasError + except ExceptionalHalt as error: + rollback_transaction(state, transient_storage) + evm.gas_left = Uint(0) + evm.output = b"" + evm.error = error + else: + set_code(state, message.current_target, contract_code) + commit_transaction(state, transient_storage) + else: + rollback_transaction(state, transient_storage) + 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.amsterdam.vm.Evm` + Items containing execution specific objects + """ + state = message.block_env.state + transient_storage = message.tx_env.transient_storage + if message.depth > STACK_DEPTH_LIMIT: + raise StackDepthLimitError("Stack depth limit reached") + + # take snapshot of state before processing the message + begin_transaction(state, transient_storage) + + if message.should_transfer_value and message.value != 0: + move_ether( + state, message.caller, message.current_target, message.value + ) + + evm = execute_code(message) + if evm.error: + # revert state to the last saved checkpoint + # since the message call resulted in an error + rollback_transaction(state, transient_storage) + else: + commit_transaction(state, transient_storage) + return evm + + +def execute_code(message: Message) -> Evm: + """ + Executes bytecode present in the `message`. + + Parameters + ---------- + message : + Transaction specific items. + + Returns + ------- + evm: `ethereum.vm.EVM` + Items containing execution specific objects + """ + 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, + 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, + ) + try: + if evm.message.code_address in PRE_COMPILED_CONTRACTS: + if message.disable_precompiles: + return evm + evm_trace(evm, PrecompileStart(evm.message.code_address)) + PRE_COMPILED_CONTRACTS[evm.message.code_address](evm) + evm_trace(evm, PrecompileEnd()) + return evm + + 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)) + evm.gas_left = Uint(0) + evm.output = b"" + evm.error = error + except Revert as error: + evm_trace(evm, OpException(error)) + evm.error = error + return evm diff --git a/src/ethereum/forks/amsterdam/vm/memory.py b/src/ethereum/forks/amsterdam/vm/memory.py new file mode 100644 index 0000000000..d4d2b47a52 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/memory.py @@ -0,0 +1,80 @@ +""" +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/amsterdam/vm/precompiled_contracts/__init__.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/__init__.py new file mode 100644 index 0000000000..f6a8cbbc12 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/__init__.py @@ -0,0 +1,56 @@ +""" +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/amsterdam/vm/precompiled_contracts/alt_bn128.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/alt_bn128.py new file mode 100644 index 0000000000..0c82ec5588 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/alt_bn128.py @@ -0,0 +1,225 @@ +""" +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 +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 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, Uint(150)) + + # 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, Uint(6000)) + + # 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, Uint(34000 * (len(data) // 192) + 45000)) + + # 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/amsterdam/vm/precompiled_contracts/blake2f.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/blake2f.py new file mode 100644 index 0000000000..0d86ba6e85 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/blake2f.py @@ -0,0 +1,41 @@ +""" +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 GAS_BLAKE2_PER_ROUND, 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, GAS_BLAKE2_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/amsterdam/vm/precompiled_contracts/bls12_381/__init__.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/__init__.py new file mode 100644 index 0000000000..731c3da4b4 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/__init__.py @@ -0,0 +1,615 @@ +""" +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: + 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: + 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/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g1.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g1.py new file mode 100644 index 0000000000..da6a984ace --- /dev/null +++ b/src/ethereum/forks/amsterdam/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 ( + GAS_BLS_G1_ADD, + GAS_BLS_G1_MAP, + GAS_BLS_G1_MUL, + 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, Uint(GAS_BLS_G1_ADD)) + + # 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) * GAS_BLS_G1_MUL * 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, Uint(GAS_BLS_G1_MAP)) + + # 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/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g2.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_g2.py new file mode 100644 index 0000000000..7f5ee65e5a --- /dev/null +++ b/src/ethereum/forks/amsterdam/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 ( + GAS_BLS_G2_ADD, + GAS_BLS_G2_MAP, + GAS_BLS_G2_MUL, + 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, Uint(GAS_BLS_G2_ADD)) + + # 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) * GAS_BLS_G2_MUL * 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, Uint(GAS_BLS_G2_MAP)) + + # 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/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py new file mode 100644 index 0000000000..6ef69dc16d --- /dev/null +++ b/src/ethereum/forks/amsterdam/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 sub-group 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("Sub-group 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("Sub-group 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/amsterdam/vm/precompiled_contracts/ecrecover.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ecrecover.py new file mode 100644 index 0000000000..1f047d3a44 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ecrecover.py @@ -0,0 +1,63 @@ +""" +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 GAS_ECRECOVER, 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, GAS_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/amsterdam/vm/precompiled_contracts/identity.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/identity.py new file mode 100644 index 0000000000..88729c96d7 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/identity.py @@ -0,0 +1,38 @@ +""" +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 + +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import GAS_IDENTITY, GAS_IDENTITY_WORD, 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(Uint(len(data))) // Uint(32) + charge_gas(evm, GAS_IDENTITY + GAS_IDENTITY_WORD * word_count) + + # OPERATION + evm.output = data diff --git a/src/ethereum/forks/amsterdam/vm/precompiled_contracts/mapping.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/mapping.py new file mode 100644 index 0000000000..cf09f008ea --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/mapping.py @@ -0,0 +1,77 @@ +""" +Precompiled Contract Addresses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Mapping of precompiled contracts their implementations. +""" +from typing import Callable, Dict + +from ...fork_types 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/amsterdam/vm/precompiled_contracts/modexp.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/modexp.py new file mode 100644 index 0000000000..3e217c402c --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/modexp.py @@ -0,0 +1,178 @@ +""" +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/amsterdam/vm/precompiled_contracts/p256verify.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/p256verify.py new file mode 100644 index 0000000000..3d5783a03f --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/p256verify.py @@ -0,0 +1,85 @@ +""" +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 GAS_P256VERIFY, 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, GAS_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/amsterdam/vm/precompiled_contracts/point_evaluation.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/point_evaluation.py new file mode 100644 index 0000000000..188f90f83f --- /dev/null +++ b/src/ethereum/forks/amsterdam/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 GAS_POINT_EVALUATION, 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, GAS_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/amsterdam/vm/precompiled_contracts/ripemd160.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ripemd160.py new file mode 100644 index 0000000000..6af1086a82 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/ripemd160.py @@ -0,0 +1,43 @@ +""" +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 + +from ethereum.utils.byte import left_pad_zero_bytes +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import GAS_RIPEMD160, GAS_RIPEMD160_WORD, 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(Uint(len(data))) // Uint(32) + charge_gas(evm, GAS_RIPEMD160 + GAS_RIPEMD160_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/amsterdam/vm/precompiled_contracts/sha256.py b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/sha256.py new file mode 100644 index 0000000000..db33a37967 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/precompiled_contracts/sha256.py @@ -0,0 +1,40 @@ +""" +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 + +from ethereum.utils.numeric import ceil32 + +from ...vm import Evm +from ...vm.gas import GAS_SHA256, GAS_SHA256_WORD, 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(Uint(len(data))) // Uint(32) + charge_gas(evm, GAS_SHA256 + GAS_SHA256_WORD * word_count) + + # OPERATION + evm.output = hashlib.sha256(data).digest() diff --git a/src/ethereum/forks/amsterdam/vm/runtime.py b/src/ethereum/forks/amsterdam/vm/runtime.py new file mode 100644 index 0000000000..acead2be90 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/runtime.py @@ -0,0 +1,68 @@ +""" +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. + + 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) + + pc += Uint(1) + + return valid_jump_destinations diff --git a/src/ethereum/forks/amsterdam/vm/stack.py b/src/ethereum/forks/amsterdam/vm/stack.py new file mode 100644 index 0000000000..f28a5b3b88 --- /dev/null +++ b/src/ethereum/forks/amsterdam/vm/stack.py @@ -0,0 +1,59 @@ +""" +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 + +from ethereum_types.numeric import U256 + +from .exceptions import StackOverflowError, StackUnderflowError + + +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 3ace6faf6bb4a4f13eb37978e09af71567db0eae Mon Sep 17 00:00:00 2001 From: nerolation Date: Tue, 1 Jul 2025 12:32:59 +0200 Subject: [PATCH 02/18] Initial implementation for EIP-7928: Block-Level Access Lists: - bal -> block_access_list; re-add custom rlp encoding for block access list - bytes to uint - move away from method-style - Update EIP-7928 implementation: system contracts at index 0, migrate to RLP - System contracts (parent hash, beacon root) now use block_access_index 0 - Transactions use block_access_index 1 to len(transactions) - Post-execution changes use block_access_index len(transactions) + 1 - Migrated from SSZ to RLP encoding as per updated EIP-7928 spec - Updated all tests to match new API and structure - Replaced tx_index with block_access_index throughout codebase - add system contract logic - add markdown docstrings - update BAL format; address comments - ssz encoding and bal validation - six ssz types - bal tests - balspecs --- .../amsterdam/block_access_lists/__init__.py | 51 ++ .../amsterdam/block_access_lists/builder.py | 356 +++++++++++ .../amsterdam/block_access_lists/rlp_utils.py | 396 +++++++++++++ .../amsterdam/block_access_lists/tracker.py | 343 +++++++++++ .../amsterdam/block_access_lists/utils.py | 521 ++++++++++++++++ src/ethereum/forks/amsterdam/blocks.py | 17 + src/ethereum/forks/amsterdam/fork.py | 52 +- src/ethereum/forks/amsterdam/rlp_types.py | 114 ++++ src/ethereum/forks/amsterdam/ssz_types.py | 97 +++ src/ethereum/forks/amsterdam/state.py | 60 +- src/ethereum/forks/amsterdam/vm/__init__.py | 7 + .../forks/amsterdam/vm/eoa_delegation.py | 4 +- .../amsterdam/vm/instructions/environment.py | 16 + .../amsterdam/vm/instructions/storage.py | 16 + .../forks/amsterdam/vm/instructions/system.py | 19 +- .../forks/amsterdam/vm/interpreter.py | 7 +- tests/osaka/conftest.py | 11 + tests/osaka/test_bal_implementation.py | 556 ++++++++++++++++++ 18 files changed, 2625 insertions(+), 18 deletions(-) create mode 100644 src/ethereum/forks/amsterdam/block_access_lists/__init__.py create mode 100644 src/ethereum/forks/amsterdam/block_access_lists/builder.py create mode 100644 src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py create mode 100644 src/ethereum/forks/amsterdam/block_access_lists/tracker.py create mode 100644 src/ethereum/forks/amsterdam/block_access_lists/utils.py create mode 100644 src/ethereum/forks/amsterdam/rlp_types.py create mode 100644 src/ethereum/forks/amsterdam/ssz_types.py create mode 100644 tests/osaka/conftest.py create mode 100644 tests/osaka/test_bal_implementation.py diff --git a/src/ethereum/forks/amsterdam/block_access_lists/__init__.py b/src/ethereum/forks/amsterdam/block_access_lists/__init__.py new file mode 100644 index 0000000000..ccd762d757 --- /dev/null +++ b/src/ethereum/forks/amsterdam/block_access_lists/__init__.py @@ -0,0 +1,51 @@ +""" +Block Access Lists (EIP-7928) implementation for Ethereum Osaka fork. +""" + +from .builder import ( + BlockAccessListBuilder, + add_balance_change, + add_code_change, + add_nonce_change, + add_storage_read, + add_storage_write, + add_touched_account, + build, +) +from .tracker import ( + StateChangeTracker, + set_transaction_index, + track_address_access, + track_balance_change, + track_code_change, + track_nonce_change, + track_storage_read, + track_storage_write, +) +from .rlp_utils import ( + compute_block_access_list_hash, + rlp_encode_block_access_list, + validate_block_access_list_against_execution, +) + +__all__ = [ + "BlockAccessListBuilder", + "StateChangeTracker", + "add_balance_change", + "add_code_change", + "add_nonce_change", + "add_storage_read", + "add_storage_write", + "add_touched_account", + "build", + "compute_block_access_list_hash", + "set_transaction_index", + "rlp_encode_block_access_list", + "track_address_access", + "track_balance_change", + "track_code_change", + "track_nonce_change", + "track_storage_read", + "track_storage_write", + "validate_block_access_list_against_execution", +] \ No newline at end of file diff --git a/src/ethereum/forks/amsterdam/block_access_lists/builder.py b/src/ethereum/forks/amsterdam/block_access_lists/builder.py new file mode 100644 index 0000000000..12dac71d5c --- /dev/null +++ b/src/ethereum/forks/amsterdam/block_access_lists/builder.py @@ -0,0 +1,356 @@ +""" +Block Access List Builder for EIP-7928 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This module implements the Block Access List builder that tracks all account +and storage accesses during block execution and constructs the final +[`BlockAccessList`]. + +The builder follows a two-phase approach: + +1. **Collection Phase**: During transaction execution, all state accesses are + recorded via the tracking functions. +2. **Build Phase**: After block execution, the accumulated data is sorted + and encoded into the final deterministic format. + +[`BlockAccessList`]: ref:ethereum.osaka.ssz_types.BlockAccessList +""" + +from dataclasses import dataclass, field +from typing import Dict, List, Set + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U32, U64, U256, Uint + +from ..fork_types import Address +from ..rlp_types import ( + AccountChanges, + BalanceChange, + BlockAccessList, + BlockAccessIndex, + CodeChange, + NonceChange, + SlotChanges, + StorageChange, +) + + +@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[Bytes, 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[Bytes] = 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. + """ + + +@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. + + [`BlockAccessList`]: ref:ethereum.osaka.ssz_types.BlockAccessList + """ + 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`] 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. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address to ensure exists. + + [`AccountData`]: ref:ethereum.osaka.block_access_lists.builder.AccountData + """ + if address not in builder.accounts: + builder.accounts[address] = AccountData() + + +def add_storage_write( + builder: BlockAccessListBuilder, + address: Address, + slot: Bytes, + block_access_index: BlockAccessIndex, + new_value: Bytes +) -> 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. Multiple writes to the same slot are tracked + separately, maintaining the order and transaction index of each change. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address whose storage is being modified. + slot : + The storage slot being written to. + block_access_index : + The block access index for this change (0 for pre-execution, 1..n for transactions, n+1 for post-execution). + new_value : + The new value being written to the storage slot. + """ + ensure_account(builder, address) + + if slot not in builder.accounts[address].storage_changes: + builder.accounts[address].storage_changes[slot] = [] + + 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: Bytes +) -> 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]. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address whose storage is being read. + slot : + The storage slot being read. + + [EIP-7928]: https://eips.ethereum.org/EIPS/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. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address whose balance changed. + block_access_index : + The block access index for this change (0 for pre-execution, 1..n for transactions, n+1 for post-execution). + post_balance : + The account balance after the change as U256. + """ + ensure_account(builder, address) + + change = BalanceChange(block_access_index=block_access_index, post_balance=post_balance) + 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. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address whose nonce changed. + block_access_index : + The block access index for this change (0 for pre-execution, 1..n for transactions, n+1 for post-execution). + new_nonce : + The new nonce value after the change. + + [`CREATE`]: ref:ethereum.osaka.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.osaka.vm.instructions.system.create2 + """ + ensure_account(builder, address) + + 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 [`SETCODE`] + operations. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address receiving new code. + block_access_index : + The block access index for this change (0 for pre-execution, 1..n for transactions, n+1 for post-execution). + new_code : + The deployed contract bytecode. + + [`CREATE`]: ref:ethereum.osaka.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.osaka.vm.instructions.system.create2 + [`SETCODE`]: ref:ethereum.osaka.vm.instructions.system.setcode + """ + ensure_account(builder, address) + + 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. + + Parameters + ---------- + builder : + The block access list builder instance. + address : + The account address that was accessed. + + [`EXTCODEHASH`]: ref:ethereum.osaka.vm.instructions.environment.extcodehash + [`BALANCE`]: ref:ethereum.osaka.vm.instructions.environment.balance + [`EXTCODESIZE`]: ref:ethereum.osaka.vm.instructions.environment.extcodesize + [`EXTCODECOPY`]: ref:ethereum.osaka.vm.instructions.environment.extcodecopy + """ + ensure_account(builder, address) + + +def build(builder: BlockAccessListBuilder) -> BlockAccessList: + """ + Build the final [`BlockAccessList`] from accumulated changes. + + 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 + + Parameters + ---------- + builder : + The block access list builder containing all tracked changes. + + Returns + ------- + block_access_list : + The final sorted and encoded block access list. + + [`BlockAccessList`]: ref:ethereum.osaka.ssz_types.BlockAccessList + """ + account_changes_list = [] + + 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 + ) + + account_changes_list.append(account_change) + + account_changes_list.sort(key=lambda x: x.address) + + return BlockAccessList(account_changes=tuple(account_changes_list)) \ No newline at end of file diff --git a/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py b/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py new file mode 100644 index 0000000000..335e4d1c42 --- /dev/null +++ b/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py @@ -0,0 +1,396 @@ +""" +Block Access List RLP Utilities for EIP-7928 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Utilities for working with Block Access Lists using RLP encoding, +as specified in EIP-7928. + +This module provides: + +- RLP encoding functions for all Block Access List types +- Hash computation using [`keccak256`] +- Validation logic to ensure structural correctness + +The encoding follows the RLP specification used throughout Ethereum. + +[`keccak256`]: ref:ethereum.crypto.hash.keccak256 +""" + +from typing import Optional + +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import Uint + +from ethereum.crypto.hash import Hash32, keccak256 + +from ..rlp_types import ( + BlockAccessList, + AccountChanges, + SlotChanges, + StorageChange, + BalanceChange, + NonceChange, + CodeChange, + MAX_TXS, + MAX_SLOTS, + MAX_ACCOUNTS, + MAX_CODE_SIZE, +) + + +def compute_block_access_list_hash(block_access_list: BlockAccessList) -> Hash32: + """ + Compute the hash of a Block Access List. + + The Block Access List is RLP-encoded and then hashed with keccak256. + + Parameters + ---------- + block_access_list : + The Block Access List to hash. + + Returns + ------- + hash : + The keccak256 hash of the RLP-encoded Block Access List. + """ + block_access_list_bytes = rlp_encode_block_access_list(block_access_list) + return keccak256(block_access_list_bytes) + + +def rlp_encode_storage_change(change: StorageChange) -> bytes: + """ + Encode a [`StorageChange`] as RLP. + + Encoded as: [block_access_index, new_value] + + Parameters + ---------- + change : + The storage change to encode. + + Returns + ------- + encoded : + The RLP-encoded storage change. + + [`StorageChange`]: ref:ethereum.osaka.rlp_types.StorageChange + """ + return rlp.encode([ + Uint(change.block_access_index), + change.new_value + ]) + + +def rlp_encode_balance_change(change: BalanceChange) -> bytes: + """ + Encode a [`BalanceChange`] as RLP. + + Encoded as: [block_access_index, post_balance] + + Parameters + ---------- + change : + The balance change to encode. + + Returns + ------- + encoded : + The RLP-encoded balance change. + + [`BalanceChange`]: ref:ethereum.osaka.rlp_types.BalanceChange + """ + return rlp.encode([ + Uint(change.block_access_index), + change.post_balance + ]) + + +def rlp_encode_nonce_change(change: NonceChange) -> bytes: + """ + Encode a [`NonceChange`] as RLP. + + Encoded as: [block_access_index, new_nonce] + + Parameters + ---------- + change : + The nonce change to encode. + + Returns + ------- + encoded : + The RLP-encoded nonce change. + + [`NonceChange`]: ref:ethereum.osaka.rlp_types.NonceChange + """ + return rlp.encode([ + Uint(change.block_access_index), + Uint(change.new_nonce) + ]) + + +def rlp_encode_code_change(change: CodeChange) -> bytes: + """ + Encode a [`CodeChange`] as RLP. + + Encoded as: [block_access_index, new_code] + + Parameters + ---------- + change : + The code change to encode. + + Returns + ------- + encoded : + The RLP-encoded code change. + + [`CodeChange`]: ref:ethereum.osaka.rlp_types.CodeChange + """ + return rlp.encode([ + Uint(change.block_access_index), + change.new_code + ]) + + +def rlp_encode_slot_changes(slot_changes: SlotChanges) -> bytes: + """ + Encode [`SlotChanges`] as RLP. + + Encoded as: [slot, [changes]] + + Parameters + ---------- + slot_changes : + The slot changes to encode. + + Returns + ------- + encoded : + The RLP-encoded slot changes. + + [`SlotChanges`]: ref:ethereum.osaka.rlp_types.SlotChanges + """ + # Encode each change as [block_access_index, new_value] + changes_list = [ + [Uint(change.block_access_index), change.new_value] + for change in slot_changes.changes + ] + + return rlp.encode([ + slot_changes.slot, + changes_list + ]) + + +def rlp_encode_account_changes(account: AccountChanges) -> bytes: + """ + Encode [`AccountChanges`] as RLP. + + Encoded as: [address, storage_changes, storage_reads, balance_changes, nonce_changes, code_changes] + + Parameters + ---------- + account : + The account changes to encode. + + Returns + ------- + encoded : + The RLP-encoded account changes. + + [`AccountChanges`]: ref:ethereum.osaka.rlp_types.AccountChanges + """ + # Encode storage_changes: [[slot, [[block_access_index, new_value], ...]], ...] + storage_changes_list = [ + [slot_changes.slot, [[Uint(c.block_access_index), c.new_value] for c in slot_changes.changes]] + for slot_changes in account.storage_changes + ] + + # Encode storage_reads: [slot1, slot2, ...] + storage_reads_list = list(account.storage_reads) + + # Encode balance_changes: [[block_access_index, post_balance], ...] + balance_changes_list = [ + [Uint(bc.block_access_index), bc.post_balance] + for bc in account.balance_changes + ] + + # Encode nonce_changes: [[block_access_index, new_nonce], ...] + nonce_changes_list = [ + [Uint(nc.block_access_index), Uint(nc.new_nonce)] + for nc in account.nonce_changes + ] + + # Encode code_changes: [[block_access_index, new_code], ...] + code_changes_list = [ + [Uint(cc.block_access_index), cc.new_code] + for cc in account.code_changes + ] + + return rlp.encode([ + account.address, + storage_changes_list, + storage_reads_list, + balance_changes_list, + nonce_changes_list, + code_changes_list + ]) + + +def rlp_encode_block_access_list(block_access_list: BlockAccessList) -> Bytes: + """ + Encode a [`BlockAccessList`] to RLP bytes. + + This function produces the final RLP representation of a block's access list, + following the EIP-7928 specification. + + Parameters + ---------- + block_access_list : + The block access list to encode. + + Returns + ------- + encoded : + The complete RLP-encoded block access list. + + [`BlockAccessList`]: ref:ethereum.osaka.rlp_types.BlockAccessList + """ + # Encode as a list of AccountChanges directly (not wrapped) + account_changes_list = [] + for account in block_access_list.account_changes: + # Each account is encoded as: + # [address, storage_changes, storage_reads, balance_changes, nonce_changes, code_changes] + storage_changes_list = [ + [slot_changes.slot, [[Uint(c.block_access_index), c.new_value] for c in slot_changes.changes]] + for slot_changes in account.storage_changes + ] + + storage_reads_list = list(account.storage_reads) + + balance_changes_list = [ + [Uint(bc.block_access_index), bc.post_balance] + for bc in account.balance_changes + ] + + nonce_changes_list = [ + [Uint(nc.block_access_index), Uint(nc.new_nonce)] + for nc in account.nonce_changes + ] + + code_changes_list = [ + [Uint(cc.block_access_index), cc.new_code] + for cc in account.code_changes + ] + + account_changes_list.append([ + account.address, + storage_changes_list, + storage_reads_list, + balance_changes_list, + nonce_changes_list, + code_changes_list + ]) + + encoded = rlp.encode(account_changes_list) + return Bytes(encoded) + + +def validate_block_access_list_against_execution( + block_access_list: BlockAccessList, + block_access_list_builder: Optional['BlockAccessListBuilder'] = None +) -> bool: + """ + Validate that a Block Access List is structurally correct and optionally matches a builder's state. + + Parameters + ---------- + block_access_list : + The Block Access List to validate. + block_access_list_builder : + Optional Block Access List builder to validate against. If provided, checks that the + Block Access List hash matches what would be built from the builder's current state. + + Returns + ------- + valid : + True if the Block Access List is structurally valid and matches the builder (if provided). + """ + # 1. Validate structural constraints + + # Check that storage changes and reads don't overlap for the same slot + for account in block_access_list.account_changes: + changed_slots = {sc.slot for sc in account.storage_changes} + read_slots = set(account.storage_reads) + + # A slot should not be in both changes and reads (per EIP-7928) + if changed_slots & read_slots: + return False + + # 2. Validate ordering (addresses should be sorted lexicographically) + addresses = [account.address for account in block_access_list.account_changes] + if addresses != sorted(addresses): + return False + + # 3. Validate all data is within bounds + max_block_access_index = MAX_TXS + 1 # 0 for pre-exec, 1..MAX_TXS for txs, MAX_TXS+1 for post-exec + for account in block_access_list.account_changes: + # Validate storage slots are sorted within each account + storage_slots = [sc.slot for sc in account.storage_changes] + if storage_slots != sorted(storage_slots): + return False + + # Check storage changes + for slot_changes in account.storage_changes: + # Check changes are sorted by block_access_index + indices = [c.block_access_index for c in slot_changes.changes] + if indices != sorted(indices): + return False + + for change in slot_changes.changes: + if change.block_access_index > max_block_access_index: + return False + + # Check balance changes are sorted by block_access_index + balance_indices = [bc.block_access_index for bc in account.balance_changes] + if balance_indices != sorted(balance_indices): + return False + + for balance_change in account.balance_changes: + if balance_change.block_access_index > max_block_access_index: + return False + + # Check nonce changes are sorted by block_access_index + nonce_indices = [nc.block_access_index for nc in account.nonce_changes] + if nonce_indices != sorted(nonce_indices): + return False + + for nonce_change in account.nonce_changes: + if nonce_change.block_access_index > max_block_access_index: + return False + + # Check code changes are sorted by block_access_index + code_indices = [cc.block_access_index for cc in account.code_changes] + if code_indices != sorted(code_indices): + return False + + for code_change in account.code_changes: + if code_change.block_access_index > max_block_access_index: + return False + if len(code_change.new_code) > MAX_CODE_SIZE: + return False + + # 4. If Block Access List builder provided, validate against it by comparing hashes + if block_access_list_builder is not None: + from .builder import build + # Build a Block Access List from the builder + expected_block_access_list = build(block_access_list_builder) + + # Compare hashes + if compute_block_access_list_hash(block_access_list) != compute_block_access_list_hash(expected_block_access_list): + return False + + return True \ No newline at end of file diff --git a/src/ethereum/forks/amsterdam/block_access_lists/tracker.py b/src/ethereum/forks/amsterdam/block_access_lists/tracker.py new file mode 100644 index 0000000000..4ae758db8f --- /dev/null +++ b/src/ethereum/forks/amsterdam/block_access_lists/tracker.py @@ -0,0 +1,343 @@ +""" +Block Access List State Change Tracker for EIP-7928 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This module provides state change tracking functionality for building Block +Access Lists during transaction execution. + +The tracker integrates with the EVM execution to capture all state accesses +and modifications, distinguishing between actual changes and no-op operations. +It maintains a cache of pre-state values to enable accurate change detection +throughout block execution. + +See [EIP-7928] for the full specification. + +[EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 +""" + +from dataclasses import dataclass, field +from typing import Dict, Optional + +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import U32, U64, U256, Uint + +from ..rlp_types import BlockAccessIndex + +from ..fork_types import Address, Account +from ..state import State, get_account, get_storage +from .builder import ( + BlockAccessListBuilder, + add_balance_change, + add_code_change, + add_nonce_change, + add_storage_read, + add_storage_write, + add_touched_account, +) + + +@dataclass +class StateChangeTracker: + """ + Tracks state changes during transaction execution for Block Access List + construction. + + This tracker maintains a cache of pre-state values and coordinates with + the [`BlockAccessListBuilder`] to record all state changes made during + block execution. It ensures that only actual changes (not no-op writes) + are recorded in the access list. + + [`BlockAccessListBuilder`]: ref:ethereum.osaka.block_access_lists.builder.BlockAccessListBuilder + """ + block_access_list_builder: BlockAccessListBuilder + """ + The builder instance that accumulates all tracked changes. + """ + + pre_storage_cache: Dict[tuple, U256] = field(default_factory=dict) + """ + Cache of pre-state storage values, keyed by (address, slot) tuples. + This cache persists across transactions within a block to track the + original state before any modifications. + """ + + current_block_access_index: int = 0 + """ + The current block access index (0 for pre-execution, 1..n for transactions, n+1 for post-execution). + """ + + +def set_transaction_index(tracker: StateChangeTracker, block_access_index: int) -> None: + """ + Set the current block access index for tracking changes. + + Must be called before processing each transaction/system contract to ensure changes + are associated with the correct block access index. + + Parameters + ---------- + tracker : + The state change tracker instance. + block_access_index : + The block access index (0 for pre-execution, 1..n for transactions, n+1 for post-execution). + """ + tracker.current_block_access_index = block_access_index + + +def capture_pre_state( + tracker: StateChangeTracker, + address: Address, + key: Bytes, + state: State +) -> U256: + """ + Capture and cache the pre-state value for a storage location. + + Retrieves the storage value from before any transactions in the current + block modified it. The value is cached to avoid repeated lookups and + to maintain consistency across multiple accesses. + + Parameters + ---------- + tracker : + The state change tracker instance. + address : + The account address containing the storage. + key : + The storage slot to read. + state : + The current execution state. + + Returns + ------- + value : + The original storage value before any block modifications. + """ + cache_key = (address, key) + if cache_key not in tracker.pre_storage_cache: + tracker.pre_storage_cache[cache_key] = get_storage(state, address, key) + return tracker.pre_storage_cache[cache_key] + + +def track_address_access(tracker: StateChangeTracker, address: Address) -> None: + """ + Track that an address was accessed. + + Records account access even when no state changes occur. This is + important for operations that read account data without modifying it. + + Parameters + ---------- + tracker : + The state change tracker instance. + address : + The account address that was accessed. + """ + add_touched_account(tracker.block_access_list_builder, address) + + +def track_storage_read( + tracker: StateChangeTracker, + address: Address, + key: Bytes, + state: State +) -> None: + """ + Track a storage read operation. + + Records that a storage slot was read and captures its pre-state value. + The slot will only appear in the final access list if it wasn't also + written to during block execution. + + Parameters + ---------- + tracker : + The state change tracker instance. + address : + The account address whose storage is being read. + key : + The storage slot being read. + state : + The current execution state. + """ + track_address_access(tracker, address) + + capture_pre_state(tracker, address, key, state) + + add_storage_read(tracker.block_access_list_builder, address, key) + + +def track_storage_write( + tracker: StateChangeTracker, + address: Address, + key: Bytes, + new_value: U256, + state: State +) -> None: + """ + Track a storage write operation. + + Records storage modifications, but only if the new value differs from + the pre-state value. No-op writes (where the value doesn't change) are + tracked as reads instead, as specified in [EIP-7928]. + + Parameters + ---------- + tracker : + The state change tracker instance. + address : + The account address whose storage is being modified. + key : + The storage slot being written to. + new_value : + The new value to write. + state : + The current execution state. + + [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 + """ + track_address_access(tracker, address) + + pre_value = capture_pre_state(tracker, address, key, state) + + value_bytes = new_value.to_be_bytes32() + + if pre_value != new_value: + add_storage_write( + tracker.block_access_list_builder, + address, + key, + BlockAccessIndex(tracker.current_block_access_index), + value_bytes + ) + else: + add_storage_read(tracker.block_access_list_builder, address, key) + + +def track_balance_change( + tracker: StateChangeTracker, + address: Address, + new_balance: U256, + state: State +) -> None: + """ + Track a balance change for an account. + + Records the new balance after any balance-affecting operation, including + transfers, gas payments, block rewards, and withdrawals. The balance is + encoded as a 16-byte value (uint128) which is sufficient for the total + ETH supply. + + Parameters + ---------- + tracker : + The state change tracker instance. + address : + The account address whose balance changed. + new_balance : + The new balance value. + state : + The current execution state. + """ + track_address_access(tracker, address) + + add_balance_change( + tracker.block_access_list_builder, + address, + BlockAccessIndex(tracker.current_block_access_index), + new_balance + ) + + +def track_nonce_change( + tracker: StateChangeTracker, + address: Address, + new_nonce: Uint, + state: State +) -> None: + """ + Track a nonce change for an account. + + Records nonce increments for both EOAs (when sending transactions) and + contracts (when performing [`CREATE`] or [`CREATE2`] operations). Deployed + contracts also have their initial nonce tracked. + + Parameters + ---------- + tracker : + The state change tracker instance. + address : + The account address whose nonce changed. + new_nonce : + The new nonce value. + state : + The current execution state. + + [`CREATE`]: ref:ethereum.osaka.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.osaka.vm.instructions.system.create2 + """ + track_address_access(tracker, address) + add_nonce_change( + tracker.block_access_list_builder, + address, + BlockAccessIndex(tracker.current_block_access_index), + U64(new_nonce) + ) + + +def track_code_change( + tracker: StateChangeTracker, + address: Address, + new_code: Bytes, + state: State +) -> None: + """ + Track a code change for contract deployment. + + Records new contract code deployments via [`CREATE`], [`CREATE2`], or + [`SETCODE`] operations. This function is called when contract bytecode + is deployed to an address. + + Parameters + ---------- + tracker : + The state change tracker instance. + address : + The address receiving the contract code. + new_code : + The deployed contract bytecode. + state : + The current execution state. + + [`CREATE`]: ref:ethereum.osaka.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.osaka.vm.instructions.system.create2 + [`SETCODE`]: ref:ethereum.osaka.vm.instructions.system.setcode + """ + track_address_access(tracker, address) + add_code_change( + tracker.block_access_list_builder, + address, + BlockAccessIndex(tracker.current_block_access_index), + new_code + ) + + +def finalize_transaction_changes( + tracker: StateChangeTracker, + state: State +) -> None: + """ + Finalize changes for the current transaction. + + This method is called at the end of each transaction execution. Currently + a no-op as all tracking is done incrementally during execution, but + provided for future extensibility. + + Parameters + ---------- + tracker : + The state change tracker instance. + state : + The current execution state. + """ + pass \ No newline at end of file diff --git a/src/ethereum/forks/amsterdam/block_access_lists/utils.py b/src/ethereum/forks/amsterdam/block_access_lists/utils.py new file mode 100644 index 0000000000..a38ebc43b1 --- /dev/null +++ b/src/ethereum/forks/amsterdam/block_access_lists/utils.py @@ -0,0 +1,521 @@ +""" +Block Access List Utilities for EIP-7928 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Utilities for working with Block Access Lists, including SSZ encoding, +hashing, and validation functions. + +This module provides: + +- SSZ encoding functions for all Block Access List types +- Hash computation using [`keccak256`] +- Validation logic to ensure structural correctness + +The encoding follows the [SSZ specification] used in Ethereum consensus layer. + +[`keccak256`]: ref:ethereum.crypto.hash.keccak256 +[SSZ specification]: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md +""" + +from typing import Union, Optional +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import Uint + +from ethereum.crypto.hash import Hash32, keccak256 + +from ..ssz_types import ( + BlockAccessList, + AccountChanges, + SlotChanges, + SlotRead, + StorageChange, + BalanceChange, + NonceChange, + CodeChange, + MAX_TRANSACTIONS, + MAX_SLOTS, + MAX_ACCOUNTS, + MAX_CODE_SIZE, +) + + +def compute_block_access_list_hash(block_access_list: BlockAccessList) -> Hash32: + """ + Compute the hash of a Block Access List. + + The Block Access List is SSZ-encoded and then hashed with keccak256. + + Parameters + ---------- + block_access_list : + The Block Access List to hash. + + Returns + ------- + hash : + The keccak256 hash of the SSZ-encoded Block Access List. + """ + block_access_list_bytes = ssz_encode_block_access_list(block_access_list) + return keccak256(block_access_list_bytes) + + +def ssz_encode_uint(value: Union[int, Uint], size: int) -> bytes: + """ + Encode an unsigned integer as SSZ (little-endian). + + Parameters + ---------- + value : + The integer value to encode. + size : + The size in bytes for the encoded output. + + Returns + ------- + encoded : + The little-endian encoded bytes. + """ + if isinstance(value, Uint): + value = int(value) + return value.to_bytes(size, 'little') + + +def ssz_encode_bytes(data: bytes) -> bytes: + """ + Encode fixed-size bytes as SSZ. + + For fixed-size byte arrays, SSZ encoding is simply the bytes themselves. + + Parameters + ---------- + data : + The bytes to encode. + + Returns + ------- + encoded : + The encoded bytes (unchanged). + """ + return data + + +def ssz_encode_list(items: tuple, encode_item_fn, max_length: int = None) -> bytes: + """ + Encode a list or tuple as SSZ. + + Handles both fixed-length and variable-length lists according to the + [SSZ specification]. Variable-length lists use offset encoding when + elements have variable size. + + Parameters + ---------- + items : + The tuple of items to encode. + encode_item_fn : + Function to encode individual items. + max_length : + Maximum list length (if specified, indicates variable-length list). + + Returns + ------- + encoded : + The SSZ-encoded list. + + [SSZ specification]: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md + """ + result = bytearray() + + if max_length is None: + # Fixed-length list/tuple: just concatenate + for item in items: + result.extend(encode_item_fn(item)) + else: + # Variable-length lists use offset encoding + item_count = len(items) + if item_count == 0: + # Empty list is encoded as just the 4-byte offset pointing to itself + return ssz_encode_uint(4, 4) + + # Calculate if items are fixed or variable size + first_item_encoded = encode_item_fn(items[0]) if items else b'' + is_fixed_size = all(len(encode_item_fn(item)) == len(first_item_encoded) for item in items) + + if is_fixed_size: + # Fixed-size elements: concatenate directly + for item in items: + result.extend(encode_item_fn(item)) + else: + # Variable-size elements: use offset encoding + # Reserve space for offsets + offset_start = 4 * item_count + data_section = bytearray() + + for item in items: + # Write offset + result.extend(ssz_encode_uint(offset_start + len(data_section), 4)) + # Encode item data + item_data = encode_item_fn(item) + data_section.extend(item_data) + + result.extend(data_section) + + return bytes(result) + + +def ssz_encode_storage_change(change: StorageChange) -> bytes: + """ + Encode a [`StorageChange`] as SSZ. + + Parameters + ---------- + change : + The storage change to encode. + + Returns + ------- + encoded : + The SSZ-encoded storage change. + + [`StorageChange`]: ref:ethereum.osaka.ssz_types.StorageChange + """ + return ( + ssz_encode_uint(change.tx_index, 2) # TxIndex as uint16 + + ssz_encode_bytes(change.new_value) # StorageValue as Bytes32 + ) + + +def ssz_encode_balance_change(change: BalanceChange) -> bytes: + """ + Encode a [`BalanceChange`] as SSZ. + + Parameters + ---------- + change : + The balance change to encode. + + Returns + ------- + encoded : + The SSZ-encoded balance change. + + [`BalanceChange`]: ref:ethereum.osaka.ssz_types.BalanceChange + """ + return ( + ssz_encode_uint(change.tx_index, 2) # TxIndex as uint16 + + ssz_encode_uint(change.post_balance, 32) # Balance as uint256 + ) + + +def ssz_encode_nonce_change(change: NonceChange) -> bytes: + """ + Encode a [`NonceChange`] as SSZ. + + Parameters + ---------- + change : + The nonce change to encode. + + Returns + ------- + encoded : + The SSZ-encoded nonce change. + + [`NonceChange`]: ref:ethereum.osaka.ssz_types.NonceChange + """ + return ( + ssz_encode_uint(change.tx_index, 2) # TxIndex as uint16 + + ssz_encode_uint(change.new_nonce, 8) # Nonce as uint64 + ) + + +def ssz_encode_code_change(change: CodeChange) -> bytes: + """ + Encode a [`CodeChange`] as SSZ. + + Code changes use variable-length encoding since contract bytecode + can vary in size up to [`MAX_CODE_SIZE`]. + + Parameters + ---------- + change : + The code change to encode. + + Returns + ------- + encoded : + The SSZ-encoded code change. + + [`CodeChange`]: ref:ethereum.osaka.ssz_types.CodeChange + [`MAX_CODE_SIZE`]: ref:ethereum.osaka.ssz_types.MAX_CODE_SIZE + """ + result = bytearray() + result.extend(ssz_encode_uint(change.tx_index, 2)) # TxIndex as uint16 + # Code is variable length, so we encode length first for variable-size containers + code_len = len(change.new_code) + # In SSZ, variable-length byte arrays are prefixed with their length + result.extend(ssz_encode_uint(code_len, 4)) + result.extend(change.new_code) + return bytes(result) + + +def ssz_encode_slot_changes(slot_changes: SlotChanges) -> bytes: + """ + Encode [`SlotChanges`] as SSZ. + + Encodes a storage slot and all changes made to it during block execution. + + Parameters + ---------- + slot_changes : + The slot changes to encode. + + Returns + ------- + encoded : + The SSZ-encoded slot changes. + + [`SlotChanges`]: ref:ethereum.osaka.ssz_types.SlotChanges + """ + result = bytearray() + result.extend(ssz_encode_bytes(slot_changes.slot)) # StorageKey as Bytes32 + # Encode the list of changes + changes_encoded = ssz_encode_list( + slot_changes.changes, + ssz_encode_storage_change, + MAX_TRANSACTIONS # max length for changes + ) + result.extend(changes_encoded) + return bytes(result) + + +def ssz_encode_slot_read(slot_read: SlotRead) -> bytes: + """ + Encode a [`SlotRead`] as SSZ. + + For read-only slots, only the slot key is encoded. + + Parameters + ---------- + slot_read : + The slot read to encode. + + Returns + ------- + encoded : + The SSZ-encoded slot read. + + [`SlotRead`]: ref:ethereum.osaka.ssz_types.SlotRead + """ + return ssz_encode_bytes(slot_read.slot) # StorageKey as Bytes32 + + +def ssz_encode_account_changes(account: AccountChanges) -> bytes: + """ + Encode [`AccountChanges`] as SSZ. + + Encodes all changes for a single account using variable-size struct + encoding with offsets for the variable-length fields. + + Parameters + ---------- + account : + The account changes to encode. + + Returns + ------- + encoded : + The SSZ-encoded account changes. + + [`AccountChanges`]: ref:ethereum.osaka.ssz_types.AccountChanges + """ + # For variable-size struct, we use offset encoding + result = bytearray() + offsets = [] + data_section = bytearray() + + # Fixed-size fields first + result.extend(ssz_encode_bytes(account.address)) # Address as Bytes20 + + # Variable-size fields use offsets + # Calculate base offset (after all fixed fields and offset values) + base_offset = 20 + (5 * 4) # address + 5 offset fields + + # Encode storage_changes + storage_changes_data = ssz_encode_list( + account.storage_changes, + ssz_encode_slot_changes, + MAX_SLOTS + ) + offsets.append(base_offset + len(data_section)) + data_section.extend(storage_changes_data) + + # Encode storage_reads + storage_reads_data = ssz_encode_list( + account.storage_reads, + ssz_encode_slot_read, + MAX_SLOTS + ) + offsets.append(base_offset + len(data_section)) + data_section.extend(storage_reads_data) + + # Encode balance_changes + balance_changes_data = ssz_encode_list( + account.balance_changes, + ssz_encode_balance_change, + MAX_TRANSACTIONS + ) + offsets.append(base_offset + len(data_section)) + data_section.extend(balance_changes_data) + + # Encode nonce_changes + nonce_changes_data = ssz_encode_list( + account.nonce_changes, + ssz_encode_nonce_change, + MAX_TRANSACTIONS + ) + offsets.append(base_offset + len(data_section)) + data_section.extend(nonce_changes_data) + + # Encode code_changes + code_changes_data = ssz_encode_list( + account.code_changes, + ssz_encode_code_change, + MAX_TRANSACTIONS + ) + offsets.append(base_offset + len(data_section)) + data_section.extend(code_changes_data) + + # Write offsets + for offset in offsets: + result.extend(ssz_encode_uint(offset, 4)) + + # Write data section + result.extend(data_section) + + return bytes(result) + + +def ssz_encode_block_access_list(block_access_list: BlockAccessList) -> Bytes: + """ + Encode a [`BlockAccessList`] to SSZ bytes. + + This is the top-level encoding function that produces the final SSZ + representation of a block's access list, following the [SSZ specification] + for Ethereum. + + Parameters + ---------- + block_access_list : + The block access list to encode. + + Returns + ------- + encoded : + The complete SSZ-encoded block access list. + + [`BlockAccessList`]: ref:ethereum.osaka.ssz_types.BlockAccessList + [SSZ specification]: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md + """ + encoded = ssz_encode_list( + block_access_list.account_changes, + ssz_encode_account_changes, + MAX_ACCOUNTS + ) + return Bytes(encoded) + + +def validate_block_access_list_against_execution( + block_access_list: BlockAccessList, + block_access_list_builder: Optional['BlockAccessListBuilder'] = None +) -> bool: + """ + Validate that a Block Access List is structurally correct and optionally matches a builder's state. + + Parameters + ---------- + block_access_list : + The Block Access List to validate. + block_access_list_builder : + Optional Block Access List builder to validate against. If provided, checks that the + Block Access List hash matches what would be built from the builder's current state. + + Returns + ------- + valid : + True if the Block Access List is structurally valid and matches the builder (if provided). + """ + # 1. Validate structural constraints + + # Check that storage changes and reads don't overlap for the same slot + for account in block_access_list.account_changes: + changed_slots = {sc.slot for sc in account.storage_changes} + read_slots = {sr.slot for sr in account.storage_reads} + + # A slot should not be in both changes and reads (per EIP-7928) + if changed_slots & read_slots: + return False + + # 2. Validate ordering (addresses should be sorted lexicographically) + addresses = [account.address for account in block_access_list.account_changes] + if addresses != sorted(addresses): + return False + + # 3. Validate all data is within bounds + max_tx_index = MAX_TRANSACTIONS - 1 + for account in block_access_list.account_changes: + # Validate storage slots are sorted within each account + storage_slots = [sc.slot for sc in account.storage_changes] + if storage_slots != sorted(storage_slots): + return False + + # Check storage changes + for slot_changes in account.storage_changes: + # Check changes are sorted by tx_index + tx_indices = [c.tx_index for c in slot_changes.changes] + if tx_indices != sorted(tx_indices): + return False + + for change in slot_changes.changes: + if change.tx_index > max_tx_index: + return False + + # Check balance changes are sorted by tx_index + balance_tx_indices = [bc.tx_index for bc in account.balance_changes] + if balance_tx_indices != sorted(balance_tx_indices): + return False + + for balance_change in account.balance_changes: + if balance_change.tx_index > max_tx_index: + return False + + # Check nonce changes are sorted by tx_index + nonce_tx_indices = [nc.tx_index for nc in account.nonce_changes] + if nonce_tx_indices != sorted(nonce_tx_indices): + return False + + for nonce_change in account.nonce_changes: + if nonce_change.tx_index > max_tx_index: + return False + + # Check code changes are sorted by tx_index + code_tx_indices = [cc.tx_index for cc in account.code_changes] + if code_tx_indices != sorted(code_tx_indices): + return False + + for code_change in account.code_changes: + if code_change.tx_index > max_tx_index: + return False + if len(code_change.new_code) > MAX_CODE_SIZE: + return False + + # 4. If Block Access List builder provided, validate against it by comparing hashes + if block_access_list_builder is not None: + from .builder import build + # Build a Block Access List from the builder + expected_block_access_list = build(block_access_list_builder) + + # Compare hashes - much simpler! + if compute_block_access_list_hash(block_access_list) != compute_block_access_list_hash(expected_block_access_list): + return False + + return True \ No newline at end of file diff --git a/src/ethereum/forks/amsterdam/blocks.py b/src/ethereum/forks/amsterdam/blocks.py index 1177e4c32d..ef9ddd8777 100644 --- a/src/ethereum/forks/amsterdam/blocks.py +++ b/src/ethereum/forks/amsterdam/blocks.py @@ -19,6 +19,7 @@ from ethereum.crypto.hash import Hash32 from .fork_types import Address, Bloom, Root +from .rlp_types import BlockAccessList from .transactions import ( AccessListTransaction, BlobTransaction, @@ -241,6 +242,14 @@ class Header: [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 """ + bal_hash: Hash32 + """ + Hash of the Block Access List containing all accounts and storage + locations accessed during block execution. Introduced in [EIP-7928]. + + [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 + """ + @slotted_freezable @dataclass @@ -294,6 +303,14 @@ class Block: A tuple of withdrawals processed in this block. """ + block_access_list: BlockAccessList + """ + Block Access List containing all accounts and storage locations accessed + during block execution. Introduced in [EIP-7928]. + + [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 + """ + @slotted_freezable @dataclass diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 1cd6375b94..e538e18cfd 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -30,6 +30,7 @@ ) from . import vm +from .block_access_lists import StateChangeTracker, compute_block_access_list_hash, build, set_transaction_index, track_balance_change from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt from .bloom import logs_bloom from .exceptions import ( @@ -243,6 +244,10 @@ def state_transition(chain: BlockChain, block: Block) -> None: withdrawals_root = root(block_output.withdrawals_trie) requests_hash = compute_requests_hash(block_output.requests) + # Build and validate Block Access List + computed_block_access_list = build(block_output.block_access_list_builder) + computed_block_access_list_hash = compute_block_access_list_hash(computed_block_access_list) + if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( f"{block_output.block_gas_used} != {block.header.gas_used}" @@ -261,6 +266,10 @@ def state_transition(chain: BlockChain, block: Block) -> None: raise InvalidBlock if requests_hash != block.header.requests_hash: raise InvalidBlock + if computed_bal_hash != block.header.bal_hash: + raise InvalidBlock + if computed_block_access_list != block.block_access_list: + raise InvalidBlock chain.blocks.append(block) if len(chain.blocks) > 255: @@ -580,6 +589,7 @@ def process_system_transaction( target_address: Address, system_contract_code: Bytes, data: Bytes, + change_tracker: Optional[StateChangeTracker] = None, ) -> MessageCallOutput: """ Process a system transaction with the given code. @@ -635,6 +645,7 @@ def process_system_transaction( accessed_storage_keys=set(), disable_precompiles=False, parent_evm=None, + change_tracker=change_tracker, ) system_tx_output = process_message_call(system_tx_message) @@ -646,6 +657,7 @@ def process_checked_system_transaction( block_env: vm.BlockEnvironment, target_address: Address, data: Bytes, + change_tracker: Optional[StateChangeTracker] = None, ) -> MessageCallOutput: """ Process a system transaction and raise an error if the contract does not @@ -678,6 +690,7 @@ def process_checked_system_transaction( target_address, system_contract_code, data, + change_tracker, ) if system_tx_output.error: @@ -693,6 +706,7 @@ def process_unchecked_system_transaction( block_env: vm.BlockEnvironment, target_address: Address, data: Bytes, + change_tracker: Optional[StateChangeTracker] = None, ) -> MessageCallOutput: """ Process a system transaction without checking if the contract contains code @@ -718,6 +732,7 @@ def process_unchecked_system_transaction( target_address, system_contract_code, data, + change_tracker, ) @@ -752,26 +767,42 @@ def apply_body( """ block_output = vm.BlockOutput() + # Initialize Block Access List state change tracker + change_tracker = StateChangeTracker(block_output.block_access_list_builder) + + # Set system transaction index for pre-execution system contracts + # EIP-7928: System contracts use bal_index 0 + set_transaction_index(change_tracker, 0) + process_unchecked_system_transaction( block_env=block_env, target_address=BEACON_ROOTS_ADDRESS, data=block_env.parent_beacon_block_root, + change_tracker=change_tracker, ) process_unchecked_system_transaction( block_env=block_env, target_address=HISTORY_STORAGE_ADDRESS, data=block_env.block_hashes[-1], # The parent hash + change_tracker=change_tracker, ) + # EIP-7928: Transactions use bal_index 1 to len(transactions) for i, tx in enumerate(map(decode_transaction, transactions)): - process_transaction(block_env, block_output, tx, Uint(i)) + set_transaction_index(change_tracker, i + 1) + process_transaction(block_env, block_output, tx, Uint(i), change_tracker) + + # EIP-7928: Post-execution uses bal_index len(transactions) + 1 + post_execution_index = len(transactions) + 1 + set_transaction_index(change_tracker, post_execution_index) - process_withdrawals(block_env, block_output, withdrawals) + process_withdrawals(block_env, block_output, withdrawals, change_tracker) process_general_purpose_requests( block_env=block_env, block_output=block_output, + change_tracker=change_tracker, ) return block_output @@ -780,6 +811,7 @@ def apply_body( def process_general_purpose_requests( block_env: vm.BlockEnvironment, block_output: vm.BlockOutput, + change_tracker: StateChangeTracker, ) -> None: """ Process all the requests in the block. @@ -801,6 +833,7 @@ def process_general_purpose_requests( block_env=block_env, target_address=WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, data=b"", + change_tracker=change_tracker, ) if len(system_withdrawal_tx_output.return_data) > 0: @@ -812,6 +845,7 @@ def process_general_purpose_requests( block_env=block_env, target_address=CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, data=b"", + change_tracker=change_tracker, ) if len(system_consolidation_tx_output.return_data) > 0: @@ -826,6 +860,7 @@ def process_transaction( block_output: vm.BlockOutput, tx: Transaction, index: Uint, + change_tracker: StateChangeTracker, ) -> None: """ Execute a transaction against the provided environment. @@ -879,13 +914,13 @@ def process_transaction( effective_gas_fee = tx.gas * effective_gas_price gas = tx.gas - intrinsic_gas - increment_nonce(block_env.state, sender) + increment_nonce(block_env.state, sender, change_tracker) sender_balance_after_gas_fee = ( Uint(sender_account.balance) - effective_gas_fee - blob_gas_fee ) set_account_balance( - block_env.state, sender, U256(sender_balance_after_gas_fee) + block_env.state, sender, U256(sender_balance_after_gas_fee), change_tracker ) access_list_addresses = set() @@ -923,6 +958,7 @@ def process_transaction( ) message = prepare_message(block_env, tx_env, tx) + message.change_tracker = change_tracker tx_output = process_message_call(message) @@ -951,7 +987,7 @@ def process_transaction( sender_balance_after_refund = get_account( block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(block_env.state, sender, sender_balance_after_refund) + set_account_balance(block_env.state, sender, sender_balance_after_refund, change_tracker) # transfer miner fees coinbase_balance_after_mining_fee = get_account( @@ -961,6 +997,7 @@ def process_transaction( block_env.state, block_env.coinbase, coinbase_balance_after_mining_fee, + change_tracker ) for address in tx_output.accounts_to_delete: @@ -989,6 +1026,7 @@ def process_withdrawals( block_env: vm.BlockEnvironment, block_output: vm.BlockOutput, withdrawals: Tuple[Withdrawal, ...], + change_tracker: StateChangeTracker, ) -> None: """ Increase the balance of the withdrawing account. @@ -1006,6 +1044,10 @@ def increase_recipient_balance(recipient: Account) -> None: modify_state(block_env.state, wd.address, increase_recipient_balance) + # Track balance change for BAL (withdrawals are tracked as system contract changes) + new_balance = get_account(block_env.state, wd.address).balance + track_balance_change(change_tracker, wd.address, U256(new_balance), block_env.state) + def check_gas_limit(gas_limit: Uint, parent_gas_limit: Uint) -> bool: """ diff --git a/src/ethereum/forks/amsterdam/rlp_types.py b/src/ethereum/forks/amsterdam/rlp_types.py new file mode 100644 index 0000000000..c87577ce76 --- /dev/null +++ b/src/ethereum/forks/amsterdam/rlp_types.py @@ -0,0 +1,114 @@ +""" +RLP Types for EIP-7928 Block-Level Access Lists +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This module defines the RLP data structures for Block-Level Access Lists +as specified in EIP-7928. These structures enable efficient encoding and +decoding of all accounts and storage locations accessed during block execution. + +The encoding follows the pattern: address -> field -> block_access_index -> change +""" + +from dataclasses import dataclass +from typing import List, Tuple + +from ethereum_types.bytes import Bytes, Bytes20, Bytes32 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U64, U256, Uint + +# Type aliases for clarity (matching EIP-7928 specification) +Address = Bytes20 +StorageKey = Bytes32 +StorageValue = Bytes32 +CodeData = Bytes +BlockAccessIndex = Uint # uint16 in the spec, but using Uint for compatibility +Balance = U256 # Post-transaction balance in wei +Nonce = U64 + +# Constants chosen to support a 630m block gas limit +MAX_TXS = 30_000 +MAX_SLOTS = 300_000 +MAX_ACCOUNTS = 300_000 +MAX_CODE_SIZE = 24_576 +MAX_CODE_CHANGES = 1 + + +@slotted_freezable +@dataclass +class StorageChange: + """ + Storage change: [block_access_index, new_value] + RLP encoded as a list + """ + block_access_index: BlockAccessIndex + new_value: StorageValue + + +@slotted_freezable +@dataclass +class BalanceChange: + """ + Balance change: [block_access_index, post_balance] + RLP encoded as a list + """ + block_access_index: BlockAccessIndex + post_balance: Balance + + +@slotted_freezable +@dataclass +class NonceChange: + """ + Nonce change: [block_access_index, new_nonce] + RLP encoded as a list + """ + block_access_index: BlockAccessIndex + new_nonce: Nonce + + +@slotted_freezable +@dataclass +class CodeChange: + """ + Code change: [block_access_index, new_code] + RLP encoded as a list + """ + block_access_index: BlockAccessIndex + new_code: CodeData + + +@slotted_freezable +@dataclass +class SlotChanges: + """ + All changes to a single storage slot: [slot, [changes]] + RLP encoded as a list + """ + slot: StorageKey + changes: Tuple[StorageChange, ...] + + +@slotted_freezable +@dataclass +class AccountChanges: + """ + All changes for a single account, grouped by field type. + RLP encoded as: [address, storage_changes, storage_reads, balance_changes, nonce_changes, code_changes] + """ + address: Address + storage_changes: Tuple[SlotChanges, ...] # slot -> [block_access_index -> new_value] + storage_reads: Tuple[StorageKey, ...] # read-only storage keys + balance_changes: Tuple[BalanceChange, ...] # [block_access_index -> post_balance] + nonce_changes: Tuple[NonceChange, ...] # [block_access_index -> new_nonce] + code_changes: Tuple[CodeChange, ...] # [block_access_index -> new_code] + + +@slotted_freezable +@dataclass +class BlockAccessList: + """ + Block-Level Access List for EIP-7928. + Contains all addresses accessed during block execution. + RLP encoded as a list of AccountChanges + """ + account_changes: Tuple[AccountChanges, ...] \ No newline at end of file diff --git a/src/ethereum/forks/amsterdam/ssz_types.py b/src/ethereum/forks/amsterdam/ssz_types.py new file mode 100644 index 0000000000..4e68ac1818 --- /dev/null +++ b/src/ethereum/forks/amsterdam/ssz_types.py @@ -0,0 +1,97 @@ +""" +SSZ Types for EIP-7928 Block-Level Access Lists +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This module defines the SSZ data structures for Block-Level Access Lists +as specified in EIP-7928. These structures enable efficient encoding and +decoding of all accounts and storage locations accessed during block execution. +""" + +from dataclasses import dataclass +from typing import List, Tuple + +from ethereum_types.bytes import Bytes, Bytes20, Bytes32 +from ethereum_types.frozen import slotted_freezable +from ethereum_types.numeric import U256, Uint + +# Type aliases for clarity +Address = Bytes20 +StorageKey = Bytes32 +StorageValue = Bytes32 +TxIndex = Uint +Balance = Bytes # uint128 - Post-transaction balance in wei (16 bytes, sufficient for total ETH supply) +Nonce = Uint + +# Constants chosen to support a 630m block gas limit +MAX_TRANSACTIONS = 30_000 +MAX_SLOTS = 300_000 +MAX_ACCOUNTS = 300_000 +MAX_CODE_SIZE = 24_576 +MAX_CODE_CHANGES = 1 + + +@slotted_freezable +@dataclass +class StorageChange: + """Single storage write: tx_index -> new_value""" + tx_index: TxIndex + new_value: StorageValue + + +@slotted_freezable +@dataclass +class BalanceChange: + """Single balance change: tx_index -> post_balance""" + tx_index: TxIndex + post_balance: Balance + + +@slotted_freezable +@dataclass +class NonceChange: + """Single nonce change: tx_index -> new_nonce""" + tx_index: TxIndex + new_nonce: Nonce + + +@slotted_freezable +@dataclass +class CodeChange: + """Single code change: tx_index -> new_code""" + tx_index: TxIndex + new_code: Bytes + + +@slotted_freezable +@dataclass +class SlotChanges: + """All changes to a single storage slot""" + slot: StorageKey + changes: Tuple[StorageChange, ...] + + + + +@slotted_freezable +@dataclass +class AccountChanges: + """ + All changes for a single account, grouped by field type. + This eliminates address redundancy across different change types. + """ + address: Address + storage_changes: Tuple[SlotChanges, ...] + storage_reads: Tuple[StorageKey, ...] + balance_changes: Tuple[BalanceChange, ...] + nonce_changes: Tuple[NonceChange, ...] + code_changes: Tuple[CodeChange, ...] + + +@slotted_freezable +@dataclass +class BlockAccessList: + """ + Block-Level Access List for EIP-7928. + Contains all addresses accessed during block execution. + """ + account_changes: Tuple[AccountChanges, ...] \ No newline at end of file diff --git a/src/ethereum/forks/amsterdam/state.py b/src/ethereum/forks/amsterdam/state.py index 7cec40084d..28feb53eb5 100644 --- a/src/ethereum/forks/amsterdam/state.py +++ b/src/ethereum/forks/amsterdam/state.py @@ -27,6 +27,11 @@ from .fork_types import EMPTY_ACCOUNT, Account, Address, Root from .trie import EMPTY_TRIE_ROOT, Trie, copy_trie, root, trie_get, trie_set +# Forward declaration for type hints +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from .block_access_lists import StateChangeTracker + @dataclass class State: @@ -471,6 +476,7 @@ def move_ether( sender_address: Address, recipient_address: Address, amount: U256, + change_tracker: "StateChangeTracker", ) -> None: """ Move funds between accounts. @@ -486,9 +492,22 @@ def increase_recipient_balance(recipient: Account) -> None: modify_state(state, sender_address, reduce_sender_balance) modify_state(state, recipient_address, increase_recipient_balance) - - -def set_account_balance(state: State, address: Address, amount: U256) -> None: + + if change_tracker is not None: + from .block_access_lists.tracker import track_balance_change + sender_new_balance = get_account(state, sender_address).balance + recipient_new_balance = get_account(state, recipient_address).balance + + track_balance_change(change_tracker, sender_address, U256(sender_new_balance), state) + track_balance_change(change_tracker, recipient_address, U256(recipient_new_balance), state) + + +def set_account_balance( + state: State, + address: Address, + amount: U256, + change_tracker: "StateChangeTracker", +) -> None: """ Sets the balance of an account. @@ -502,15 +521,22 @@ def set_account_balance(state: State, address: Address, amount: U256) -> None: amount: The amount that needs to set in balance. + + change_tracker: + Change tracker to record balance changes. """ def set_balance(account: Account) -> None: account.balance = amount modify_state(state, address, set_balance) + + if change_tracker is not None: + from .block_access_lists.tracker import track_balance_change + track_balance_change(change_tracker, address, amount, state) -def increment_nonce(state: State, address: Address) -> None: +def increment_nonce(state: State, address: Address, change_tracker: "StateChangeTracker") -> None: """ Increments the nonce of an account. @@ -521,15 +547,33 @@ def increment_nonce(state: State, address: Address) -> None: address: Address of the account whose nonce needs to be incremented. + + change_tracker: + Change tracker for EIP-7928. """ def increase_nonce(sender: Account) -> None: sender.nonce += Uint(1) modify_state(state, address, increase_nonce) + + # Track nonce change for Block Access List (for ALL accounts and ALL nonce changes) + # This includes: + # - EOA senders (transaction nonce increments) + # - Contracts performing CREATE/CREATE2 + # - Deployed contracts + # - EIP-7702 authorities + from .block_access_lists.tracker import track_nonce_change + account = get_account(state, address) + track_nonce_change(change_tracker, address, account.nonce, state) -def set_code(state: State, address: Address, code: Bytes) -> None: +def set_code( + state: State, + address: Address, + code: Bytes, + change_tracker: "StateChangeTracker", +) -> None: """ Sets Account code. @@ -543,12 +587,18 @@ def set_code(state: State, address: Address, code: Bytes) -> None: code: The bytecode that needs to be set. + + change_tracker: + Change tracker for EIP-7928. """ def write_code(sender: Account) -> None: sender.code = code modify_state(state, address, write_code) + + from .block_access_lists.tracker import track_code_change + track_code_change(change_tracker, address, code, state) def get_storage_original(state: State, address: Address, key: Bytes32) -> U256: diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index 033293a5fd..f3de485fdf 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -22,12 +22,17 @@ from ethereum.crypto.hash import Hash32 from ethereum.exceptions import EthereumException +from ..block_access_lists import BlockAccessListBuilder from ..blocks import Log, Receipt, Withdrawal from ..fork_types import Address, Authorization, VersionedHash from ..state import State, TransientStorage from ..transactions import LegacyTransaction from ..trie import Trie +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from ..block_access_lists import StateChangeTracker + __all__ = ("Environment", "Evm", "Message") @@ -90,6 +95,7 @@ class BlockOutput: ) blob_gas_used: U64 = U64(0) requests: List[Bytes] = field(default_factory=list) + block_access_list_builder: BlockAccessListBuilder = field(default_factory=BlockAccessListBuilder) @dataclass @@ -133,6 +139,7 @@ class Message: accessed_storage_keys: Set[Tuple[Address, Bytes32]] disable_precompiles: bool parent_evm: Optional["Evm"] + change_tracker: Optional["StateChangeTracker"] = None @dataclass diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py index 1fe2e1e7bd..ecf64b524f 100644 --- a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -195,9 +195,9 @@ def set_delegation(message: Message) -> U256: code_to_set = b"" else: code_to_set = EOA_DELEGATION_MARKER + auth.address - set_code(state, authority, code_to_set) + set_code(state, authority, code_to_set, message.change_tracker) - increment_nonce(state, authority) + increment_nonce(state, authority, message.change_tracker) if message.code_address is None: raise InvalidBlock("Invalid type 4 transaction: no target") diff --git a/src/ethereum/forks/amsterdam/vm/instructions/environment.py b/src/ethereum/forks/amsterdam/vm/instructions/environment.py index 226b3d3bb3..6d144a0087 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/environment.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/environment.py @@ -86,6 +86,10 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. balance = get_account(evm.message.block_env.state, address).balance + + if evm.message.change_tracker: + from ...block_access_lists.tracker import track_address_access + track_address_access(evm.message.change_tracker, address) push(evm.stack, balance) @@ -352,6 +356,10 @@ def extcodesize(evm: Evm) -> None: # OPERATION code = get_account(evm.message.block_env.state, address).code + + if evm.message.change_tracker: + from ...block_access_lists.tracker import track_address_access + track_address_access(evm.message.change_tracker, address) codesize = U256(len(code)) push(evm.stack, codesize) @@ -394,6 +402,10 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.message.block_env.state, address).code + + if evm.message.change_tracker: + from ...block_access_lists.tracker import track_address_access + track_address_access(evm.message.change_tracker, address) value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -480,6 +492,10 @@ def extcodehash(evm: Evm) -> None: # OPERATION account = get_account(evm.message.block_env.state, address) + + if evm.message.change_tracker: + from ...block_access_lists.tracker import track_address_access + track_address_access(evm.message.change_tracker, address) if account == EMPTY_ACCOUNT: codehash = U256(0) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py index 65a0d5a9b6..38ba054356 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -59,6 +59,15 @@ def sload(evm: Evm) -> None: value = get_storage( evm.message.block_env.state, evm.message.current_target, key ) + + if evm.message.change_tracker: + from ...block_access_lists.tracker import track_storage_read + track_storage_read( + evm.message.change_tracker, + evm.message.current_target, + key, + evm.message.block_env.state + ) push(evm.stack, value) @@ -127,6 +136,13 @@ def sstore(evm: Evm) -> None: if evm.message.is_static: raise WriteInStaticContext set_storage(state, evm.message.current_target, key, new_value) + + if evm.message.change_tracker: + from ...block_access_lists.tracker import track_storage_write + track_storage_write( + evm.message.change_tracker, + evm.message.current_target, key, new_value, state + ) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index d7308821bd..562c0b59be 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -108,12 +108,12 @@ def generic_create( evm.message.block_env.state, contract_address ) or account_has_storage(evm.message.block_env.state, contract_address): increment_nonce( - evm.message.block_env.state, evm.message.current_target + evm.message.block_env.state, evm.message.current_target, evm.message.change_tracker ) push(evm.stack, U256(0)) return - increment_nonce(evm.message.block_env.state, evm.message.current_target) + increment_nonce(evm.message.block_env.state, evm.message.current_target, evm.message.change_tracker) child_message = Message( block_env=evm.message.block_env, @@ -133,7 +133,13 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=False, parent_evm=evm, + change_tracker=evm.message.change_tracker, ) + + if evm.message.change_tracker: + from ...block_access_lists.tracker import track_address_access + track_address_access(evm.message.change_tracker, contract_address) + child_evm = process_create_message(child_message) if child_evm.error: @@ -323,7 +329,13 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=disable_precompiles, parent_evm=evm, + change_tracker=evm.message.change_tracker, ) + + if evm.message.change_tracker: + from ...block_access_lists.tracker import track_address_access + track_address_access(evm.message.change_tracker, to) + child_evm = process_message(child_message) if child_evm.error: @@ -554,6 +566,7 @@ def selfdestruct(evm: Evm) -> None: originator, beneficiary, originator_balance, + evm.message.change_tracker ) # register account for deletion only if it was created @@ -561,7 +574,7 @@ def selfdestruct(evm: Evm) -> None: if originator in evm.message.block_env.state.created_accounts: # If beneficiary is the same as originator, then # the ether is burnt. - set_account_balance(evm.message.block_env.state, originator, U256(0)) + set_account_balance(evm.message.block_env.state, originator, U256(0), evm.message.change_tracker) evm.accounts_to_delete.add(originator) # HALT the execution diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index fb893aaa6b..26afdfefb6 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -192,7 +192,7 @@ def process_create_message(message: Message) -> Evm: # added to SELFDESTRUCT by EIP-6780. mark_account_created(state, message.current_target) - increment_nonce(state, message.current_target) + increment_nonce(state, message.current_target, message.change_tracker) evm = process_message(message) if not evm.error: contract_code = evm.output @@ -210,7 +210,7 @@ def process_create_message(message: Message) -> Evm: evm.output = b"" evm.error = error else: - set_code(state, message.current_target, contract_code) + set_code(state, message.current_target, contract_code, message.change_tracker) commit_transaction(state, transient_storage) else: rollback_transaction(state, transient_storage) @@ -241,7 +241,8 @@ def process_message(message: Message) -> Evm: if message.should_transfer_value and message.value != 0: move_ether( - state, message.caller, message.current_target, message.value + state, message.caller, message.current_target, message.value, + message.change_tracker ) evm = execute_code(message) diff --git a/tests/osaka/conftest.py b/tests/osaka/conftest.py new file mode 100644 index 0000000000..ed5fd82cf9 --- /dev/null +++ b/tests/osaka/conftest.py @@ -0,0 +1,11 @@ +""" +Minimal conftest for osaka BAL tests. +""" + +import pytest + + +def pytest_configure(config): + """Configure custom markers.""" + config.addinivalue_line("markers", "bal: mark test as BAL-related") + config.addinivalue_line("markers", "integration: mark test as integration test") \ No newline at end of file diff --git a/tests/osaka/test_bal_implementation.py b/tests/osaka/test_bal_implementation.py new file mode 100644 index 0000000000..fdbd56d205 --- /dev/null +++ b/tests/osaka/test_bal_implementation.py @@ -0,0 +1,556 @@ +""" +Comprehensive tests for Block Access List (BAL) implementation in EIP-7928. + +This module tests the complete BAL implementation including: +- Core functionality (tracking, building, validation) +- State modifications and nonce tracking +- Integration with VM instructions +- Edge cases and error handling +""" + +import ast +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from ethereum_types.bytes import Bytes, Bytes20, Bytes32 +from ethereum_types.numeric import U32, U64, U256, Uint + +from ethereum.osaka.block_access_lists import ( + BlockAccessListBuilder, + StateChangeTracker, + add_storage_write, + add_storage_read, + add_balance_change, + add_nonce_change, + add_code_change, + add_touched_account, + build, +) +from ethereum.osaka.rlp_types import ( + AccountChanges, + BalanceChange, + BlockAccessList, + BlockAccessIndex, + CodeChange, + NonceChange, + SlotChanges, + StorageChange, + MAX_CODE_CHANGES, +) + + +class TestBALCore: + """Test core BAL functionality.""" + + def test_bal_builder_initialization(self): + """Test BAL builder initializes correctly.""" + builder = BlockAccessListBuilder() + assert builder.accounts == {} + + def test_bal_builder_add_storage_write(self): + """Test adding storage writes to BAL builder.""" + builder = BlockAccessListBuilder() + address = Bytes20(b'\x01' * 20) + slot = Bytes32(b'\x02' * 32) + value = Bytes32(b'\x03' * 32) + + add_storage_write(builder, address, slot, BlockAccessIndex(0), value) + + assert address in builder.accounts + assert slot in builder.accounts[address].storage_changes + assert len(builder.accounts[address].storage_changes[slot]) == 1 + + change = builder.accounts[address].storage_changes[slot][0] + assert change.block_access_index == 0 + assert change.new_value == value + + def test_bal_builder_add_storage_read(self): + """Test adding storage reads to BAL builder.""" + builder = BlockAccessListBuilder() + address = Bytes20(b'\x01' * 20) + slot = Bytes32(b'\x02' * 32) + + add_storage_read(builder, address, slot) + + assert address in builder.accounts + assert slot in builder.accounts[address].storage_reads + + def test_bal_builder_add_balance_change(self): + """Test adding balance changes to BAL builder.""" + builder = BlockAccessListBuilder() + address = Bytes20(b'\x01' * 20) + balance = Bytes(b'\x00' * 16) # uint128 + + add_balance_change(builder, address, BlockAccessIndex(0), balance) + + assert address in builder.accounts + assert len(builder.accounts[address].balance_changes) == 1 + + change = builder.accounts[address].balance_changes[0] + assert change.block_access_index == 0 + assert change.post_balance == balance + + def test_bal_builder_add_nonce_change(self): + """Test adding nonce changes to BAL builder.""" + builder = BlockAccessListBuilder() + address = Bytes20(b'\x01' * 20) + nonce = 42 + + add_nonce_change(builder, address, BlockAccessIndex(0), U64(nonce)) + + assert address in builder.accounts + assert len(builder.accounts[address].nonce_changes) == 1 + + change = builder.accounts[address].nonce_changes[0] + assert change.block_access_index == 0 + assert change.new_nonce == U64(42) + + def test_bal_builder_add_code_change(self): + """Test adding code changes to BAL builder.""" + builder = BlockAccessListBuilder() + address = Bytes20(b'\x01' * 20) + code = Bytes(b'\x60\x80\x60\x40') + + add_code_change(builder, address, BlockAccessIndex(0), code) + + assert address in builder.accounts + assert len(builder.accounts[address].code_changes) == 1 + + change = builder.accounts[address].code_changes[0] + assert change.block_access_index == 0 + assert change.new_code == code + + def test_bal_builder_touched_account(self): + """Test adding touched accounts without changes.""" + builder = BlockAccessListBuilder() + address = Bytes20(b'\x01' * 20) + + add_touched_account(builder, address) + + assert address in builder.accounts + assert builder.accounts[address].storage_changes == {} + assert builder.accounts[address].storage_reads == set() + assert builder.accounts[address].balance_changes == [] + assert builder.accounts[address].nonce_changes == [] + assert builder.accounts[address].code_changes == [] + + def test_bal_builder_build_complete(self): + """Test building a complete BlockAccessList.""" + builder = BlockAccessListBuilder() + + # Add various changes + address1 = Bytes20(b'\x01' * 20) + address2 = Bytes20(b'\x02' * 20) + slot1 = Bytes32(b'\x03' * 32) + slot2 = Bytes32(b'\x04' * 32) + + # Address 1: storage write and read + add_storage_write(builder, address1, slot1, BlockAccessIndex(1), Bytes32(b'\x05' * 32)) + add_storage_read(builder, address1, slot2) + add_balance_change(builder, address1, BlockAccessIndex(1), Bytes(b'\x00' * 16)) + + # Address 2: only touched + add_touched_account(builder, address2) + + # Build BAL + block_access_list = build(builder) + + assert isinstance(block_access_list, BlockAccessList) + assert len(block_access_list.account_changes) == 2 + + # Verify sorting by address + assert block_access_list.account_changes[0].address == address1 + assert block_access_list.account_changes[1].address == address2 + + # Verify address1 changes + acc1 = block_access_list.account_changes[0] + assert len(acc1.storage_changes) == 1 + assert len(acc1.storage_reads) == 1 + assert acc1.storage_reads[0] == slot2 # Direct StorageKey + assert len(acc1.balance_changes) == 1 + + # Verify address2 is empty + acc2 = block_access_list.account_changes[1] + assert len(acc2.storage_changes) == 0 + assert len(acc2.storage_reads) == 0 + assert len(acc2.balance_changes) == 0 + + +class TestBALTracker: + """Test BAL state change tracker functionality.""" + + def test_tracker_initialization(self): + """Test tracker initializes with BAL builder.""" + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + assert tracker.block_access_list_builder is builder + assert tracker.pre_storage_cache == {} + assert tracker.current_block_access_index == 0 + + def test_tracker_set_transaction_index(self): + """Test setting block access index.""" + from ethereum.osaka.block_access_lists import set_transaction_index + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + + set_transaction_index(tracker, 5) + assert tracker.current_block_access_index == 5 + # Pre-storage cache should persist across transactions + assert tracker.pre_storage_cache == {} + + @patch('ethereum.osaka.block_access_lists.tracker.get_storage') + def test_tracker_capture_pre_state(self, mock_get_storage): + """Test capturing pre-state values.""" + from ethereum.osaka.block_access_lists.tracker import capture_pre_state + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + + mock_state = MagicMock() + address = Bytes20(b'\x01' * 20) + slot = Bytes32(b'\x02' * 32) + expected_value = U256(42) + + mock_get_storage.return_value = expected_value + + # First call should fetch from state + value = capture_pre_state(tracker, address, slot, mock_state) + assert value == expected_value + mock_get_storage.assert_called_once_with(mock_state, address, slot) + + # Second call should use cache + mock_get_storage.reset_mock() + value2 = capture_pre_state(tracker, address, slot, mock_state) + assert value2 == expected_value + mock_get_storage.assert_not_called() + + @patch('ethereum.osaka.block_access_lists.tracker.capture_pre_state') + def test_tracker_storage_write_actual_change(self, mock_capture): + """Test tracking storage write with actual change.""" + from ethereum.osaka.block_access_lists.tracker import track_storage_write + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + tracker.current_block_access_index = 1 + + mock_state = MagicMock() + address = Bytes20(b'\x01' * 20) + slot = Bytes32(b'\x02' * 32) + old_value = U256(42) + new_value = U256(100) + + mock_capture.return_value = old_value + + track_storage_write(tracker, address, slot, new_value, mock_state) + + # Should add storage write since value changed + assert address in builder.accounts + assert slot in builder.accounts[address].storage_changes + assert len(builder.accounts[address].storage_changes[slot]) == 1 + + change = builder.accounts[address].storage_changes[slot][0] + assert change.block_access_index == 1 + assert change.new_value == new_value.to_be_bytes32() + + @patch('ethereum.osaka.block_access_lists.tracker.capture_pre_state') + def test_tracker_storage_write_no_change(self, mock_capture): + """Test tracking storage write with no actual change.""" + from ethereum.osaka.block_access_lists.tracker import track_storage_write + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + tracker.current_block_access_index = 1 + + mock_state = MagicMock() + address = Bytes20(b'\x01' * 20) + slot = Bytes32(b'\x02' * 32) + same_value = U256(42) + + mock_capture.return_value = same_value + + track_storage_write(tracker, address, slot, same_value, mock_state) + + # Should add storage read since value didn't change + assert address in builder.accounts + assert slot in builder.accounts[address].storage_reads + assert slot not in builder.accounts[address].storage_changes + + def test_tracker_balance_change(self): + """Test tracking balance changes.""" + from ethereum.osaka.block_access_lists.tracker import track_balance_change + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + tracker.current_block_access_index = 2 + + mock_state = MagicMock() + address = Bytes20(b'\x01' * 20) + new_balance = U256(1000) + + track_balance_change(tracker, address, new_balance, mock_state) + + assert address in builder.accounts + assert len(builder.accounts[address].balance_changes) == 1 + + change = builder.accounts[address].balance_changes[0] + assert change.block_access_index == 2 + # Balance is stored as U256 per EIP-7928 + assert change.post_balance == new_balance + + def test_tracker_nonce_change(self): + """Test tracking nonce changes.""" + from ethereum.osaka.block_access_lists.tracker import track_nonce_change + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + tracker.current_block_access_index = 3 + + mock_state = MagicMock() + address = Bytes20(b'\x01' * 20) + new_nonce = U64(10) + + track_nonce_change(tracker, address, new_nonce, mock_state) + + assert address in builder.accounts + assert len(builder.accounts[address].nonce_changes) == 1 + + change = builder.accounts[address].nonce_changes[0] + assert change.block_access_index == 3 + assert change.new_nonce == new_nonce + + def test_tracker_code_change(self): + """Test tracking code changes.""" + from ethereum.osaka.block_access_lists.tracker import track_code_change + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + tracker.current_block_access_index = 1 + + mock_state = MagicMock() + address = Bytes20(b'\x01' * 20) + new_code = Bytes(b'\x60\x80\x60\x40') + + track_code_change(tracker, address, new_code, mock_state) + + assert address in builder.accounts + assert len(builder.accounts[address].code_changes) == 1 + + change = builder.accounts[address].code_changes[0] + assert change.block_access_index == 1 + assert change.new_code == new_code + + +class TestBALIntegration: + """Test BAL integration with block execution.""" + + def test_system_contract_indices(self): + """Test that system contracts use block_access_index 0.""" + builder = BlockAccessListBuilder() + + # Simulate pre-execution system contract changes + beacon_roots_addr = Bytes20(b'\x00' * 19 + b'\x02') + history_addr = Bytes20(b'\x00' * 19 + b'\x35') + + # These should use index 0 + add_storage_write(builder, beacon_roots_addr, Bytes32(b'\x00' * 32), BlockAccessIndex(0), Bytes32(b'\x01' * 32)) + add_storage_write(builder, history_addr, Bytes32(b'\x00' * 32), BlockAccessIndex(0), Bytes32(b'\x02' * 32)) + + block_access_list = build(builder) + + for account in block_access_list.account_changes: + if account.address in [beacon_roots_addr, history_addr]: + for slot_changes in account.storage_changes: + for change in slot_changes.changes: + assert change.block_access_index == 0 + + def test_transaction_indices(self): + """Test that transactions use indices 1 to len(transactions).""" + builder = BlockAccessListBuilder() + + # Simulate 3 transactions + for tx_num in range(1, 4): + address = Bytes20(tx_num.to_bytes(20, 'big')) + # Transactions should use indices 1, 2, 3 + add_balance_change(builder, address, BlockAccessIndex(tx_num), Bytes(b'\x00' * 16)) + + block_access_list = build(builder) + + assert len(block_access_list.account_changes) == 3 + for i, account in enumerate(block_access_list.account_changes): + assert len(account.balance_changes) == 1 + assert account.balance_changes[0].block_access_index == i + 1 + + def test_post_execution_index(self): + """Test that post-execution changes use index len(transactions) + 1.""" + builder = BlockAccessListBuilder() + num_transactions = 5 + + # Simulate withdrawal (post-execution) + withdrawal_addr = Bytes20(b'\xff' * 20) + post_exec_index = num_transactions + 1 + + add_balance_change(builder, withdrawal_addr, BlockAccessIndex(post_exec_index), Bytes(b'\x00' * 16)) + + block_access_list = build(builder) + + for account in block_access_list.account_changes: + if account.address == withdrawal_addr: + assert len(account.balance_changes) == 1 + assert account.balance_changes[0].block_access_index == post_exec_index + + def test_mixed_indices_ordering(self): + """Test that mixed indices are properly ordered in the BAL.""" + builder = BlockAccessListBuilder() + address = Bytes20(b'\x01' * 20) + + # Add changes with different indices (out of order) + add_balance_change(builder, address, BlockAccessIndex(3), Bytes(b'\x03' * 16)) + add_balance_change(builder, address, BlockAccessIndex(1), Bytes(b'\x01' * 16)) + add_balance_change(builder, address, BlockAccessIndex(2), Bytes(b'\x02' * 16)) + add_balance_change(builder, address, BlockAccessIndex(0), Bytes(b'\x00' * 16)) + + block_access_list = build(builder) + + assert len(block_access_list.account_changes) == 1 + account = block_access_list.account_changes[0] + assert len(account.balance_changes) == 4 + + # Should be sorted by block_access_index + for i in range(4): + assert account.balance_changes[i].block_access_index == i + assert account.balance_changes[i].post_balance == bytes([i]) * 16 + + +class TestRLPEncoding: + """Test RLP encoding of BAL structures.""" + + def test_rlp_encoding_import(self): + """Test that RLP encoding utilities can be imported.""" + from ethereum.osaka.block_access_lists import rlp_encode_block_access_list, compute_block_access_list_hash + assert rlp_encode_block_access_list is not None + assert compute_block_access_list_hash is not None + + def test_rlp_encode_simple_bal(self): + """Test RLP encoding of a simple BAL.""" + from ethereum.osaka.block_access_lists import rlp_encode_block_access_list + + builder = BlockAccessListBuilder() + address = Bytes20(b'\x01' * 20) + + add_balance_change(builder, address, BlockAccessIndex(1), Bytes(b'\x00' * 16)) + + block_access_list = build(builder) + encoded = rlp_encode_block_access_list(block_access_list) + + # Should produce valid RLP bytes + assert isinstance(encoded, (bytes, Bytes)) + assert len(encoded) > 0 + + def test_bal_hash_computation(self): + """Test BAL hash computation using RLP.""" + from ethereum.osaka.block_access_lists import compute_block_access_list_hash + + builder = BlockAccessListBuilder() + address = Bytes20(b'\x01' * 20) + + add_storage_write(builder, address, Bytes32(b'\x02' * 32), BlockAccessIndex(1), Bytes32(b'\x03' * 32)) + + block_access_list = build(builder) + hash_val = compute_block_access_list_hash(block_access_list) + + # Should produce a 32-byte hash + assert len(hash_val) == 32 + + # Same BAL should produce same hash + hash_val2 = compute_block_access_list_hash(block_access_list) + assert hash_val == hash_val2 + + def test_rlp_encode_complex_bal(self): + """Test RLP encoding of a complex BAL with multiple change types.""" + from ethereum.osaka.block_access_lists import rlp_encode_block_access_list + + builder = BlockAccessListBuilder() + + # Add various types of changes + address = Bytes20(b'\x01' * 20) + slot = Bytes32(b'\x02' * 32) + + # Pre-execution (index 0) + add_storage_write(builder, address, slot, BlockAccessIndex(0), Bytes32(b'\x03' * 32)) + + # Transaction (index 1) + add_balance_change(builder, address, BlockAccessIndex(1), Bytes(b'\x00' * 16)) + add_nonce_change(builder, address, BlockAccessIndex(1), U64(1)) + + # Post-execution (index 2) + add_code_change(builder, address, BlockAccessIndex(2), Bytes(b'\x60\x80')) + + block_access_list = build(builder) + encoded = rlp_encode_block_access_list(block_access_list) + + # Should produce valid RLP bytes + assert isinstance(encoded, (bytes, Bytes)) + assert len(encoded) > 0 + + +class TestEdgeCases: + """Test edge cases and error handling.""" + + def test_empty_bal(self): + """Test building an empty BAL.""" + builder = BlockAccessListBuilder() + block_access_list = build(builder) + + assert isinstance(block_access_list, BlockAccessList) + assert len(block_access_list.account_changes) == 0 + + def test_multiple_changes_same_slot(self): + """Test multiple changes to the same storage slot.""" + builder = BlockAccessListBuilder() + address = Bytes20(b'\x01' * 20) + slot = Bytes32(b'\x02' * 32) + + # Multiple writes to same slot at different indices + add_storage_write(builder, address, slot, BlockAccessIndex(0), Bytes32(b'\x00' * 32)) + add_storage_write(builder, address, slot, BlockAccessIndex(1), Bytes32(b'\x01' * 32)) + add_storage_write(builder, address, slot, BlockAccessIndex(2), Bytes32(b'\x02' * 32)) + + block_access_list = build(builder) + + assert len(block_access_list.account_changes) == 1 + account = block_access_list.account_changes[0] + assert len(account.storage_changes) == 1 + + slot_changes = account.storage_changes[0] + assert slot_changes.slot == slot + assert len(slot_changes.changes) == 3 + + # Changes should be sorted by index + for i in range(3): + assert slot_changes.changes[i].block_access_index == i + + def test_max_code_changes_constant(self): + """Test that MAX_CODE_CHANGES constant is available.""" + assert MAX_CODE_CHANGES == 1 + + def test_address_sorting(self): + """Test that addresses are sorted lexicographically in BAL.""" + builder = BlockAccessListBuilder() + + # Add addresses in reverse order + addresses = [ + Bytes20(b'\xff' * 20), + Bytes20(b'\xaa' * 20), + Bytes20(b'\x11' * 20), + Bytes20(b'\x00' * 20), + ] + + for addr in addresses: + add_touched_account(builder, addr) + + block_access_list = build(builder) + + # Should be sorted lexicographically + sorted_addresses = sorted(addresses) + for i, account in enumerate(block_access_list.account_changes): + assert account.address == sorted_addresses[i] + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file From 208e5ec1a0e027e8dbbc0822bc2b78f76cd7fead Mon Sep 17 00:00:00 2001 From: fselmo Date: Wed, 20 Aug 2025 09:54:29 -0600 Subject: [PATCH 03/18] Fixes to communicate with BALs EEST branch: - fix(bal): Initialize the state tracker before system contract calls - We were missing system contract calls to beacon roots and history contracts. This change initializes the state tracker before system contract calls and passes the tracker to these calls if post-Amsterdam. - fix(docs): Fix issues with toxenvs: lint, doc, json_infra - fix(t8n): Only initialize the bal_change_tracker for amsterdam - feat(fork criteria): Index upcoming forks for better ordering / fix issues - chore(forks): Fix issues from lint after rebase with Osaka latest - fix(setuptools): Update packages to include amsterdam - chore(lint): Fix 'tox -e static' issues - Fix bug in tracker Manually cherry-picked from e72991bf3876563900d5c2bcc2442b0a1eeb439f Author: nerolation - chore(tests): Attempt to resolve issues with CI tests - chore(lint): fix issues from running ``tox -e static`` locally - refactor(bal): Send BAL as a list over t8n tool - fix(amsterdam): Add change tracker to state test in t8n - chore(lint,tests): Fix tests after moving bal from osaka -> amsterdam - chore(forks): Move bals from Osaka to Amsterdam - chore(lint): Fix lint issues - refactor(bal): Send the full bal object and bal_hash over t8n - If we send the full object over JSON, we can model_validate() on ESST. - If we send the hash, once we fill the pydantic model, we can get the rlp and the hash and validate that our objects match while only really validating the parts of the BAL we are interested in for each test. - chore: point to working eest branch - chore(bals): Remove unused SSZ utils.py The SSZ implementation is no longer needed as we are now using RLP - refactor(bals): Clean up BAL module types and imports - Bytes -> Bytes32 type for storage slots - Remove unused imports / fix imports / fix linting - Update function signatures to match tracker - fix(bals-tx-index): Track bal indexes in t8n Keep track of BAL index state in t8n as is done in the Osaka ``fork.py`` implementation. --- pyproject.toml | 4 + .../amsterdam/block_access_lists/__init__.py | 14 +- .../amsterdam/block_access_lists/builder.py | 246 +++++--- .../amsterdam/block_access_lists/rlp_utils.py | 230 ++++++++ .../amsterdam/block_access_lists/tracker.py | 180 +++--- .../{forks => }/amsterdam/rlp_types.py | 41 +- .../amsterdam/block_access_lists/rlp_utils.py | 396 ------------- .../amsterdam/block_access_lists/utils.py | 521 ----------------- src/ethereum/forks/amsterdam/blocks.py | 17 - src/ethereum/forks/amsterdam/fork.py | 71 +-- src/ethereum/forks/amsterdam/ssz_types.py | 97 --- src/ethereum/forks/amsterdam/state.py | 60 +- src/ethereum/forks/amsterdam/vm/__init__.py | 7 - .../forks/amsterdam/vm/eoa_delegation.py | 4 +- .../amsterdam/vm/instructions/environment.py | 16 - .../amsterdam/vm/instructions/storage.py | 16 - .../forks/amsterdam/vm/instructions/system.py | 19 +- .../forks/amsterdam/vm/interpreter.py | 7 +- src/ethereum/genesis.py | 7 + src/ethereum_spec_tools/evm_tools/daemon.py | 4 +- .../evm_tools/t8n/__init__.py | 132 ++++- .../evm_tools/t8n/t8n_types.py | 96 +++ tests/amsterdam/conftest.py | 12 + .../test_bal_implementation.py | 550 ++++++++++-------- tests/amsterdam/test_rlp.py | 178 ++++++ tests/osaka/conftest.py | 11 - whitelist.txt | 9 + 27 files changed, 1256 insertions(+), 1689 deletions(-) rename src/ethereum/{forks => }/amsterdam/block_access_lists/__init__.py (93%) rename src/ethereum/{forks => }/amsterdam/block_access_lists/builder.py (67%) create mode 100644 src/ethereum/amsterdam/block_access_lists/rlp_utils.py rename src/ethereum/{forks => }/amsterdam/block_access_lists/tracker.py (75%) rename src/ethereum/{forks => }/amsterdam/rlp_types.py (75%) delete mode 100644 src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py delete mode 100644 src/ethereum/forks/amsterdam/block_access_lists/utils.py delete mode 100644 src/ethereum/forks/amsterdam/ssz_types.py create mode 100644 tests/amsterdam/conftest.py rename tests/{osaka => amsterdam}/test_bal_implementation.py (66%) create mode 100644 tests/amsterdam/test_rlp.py delete mode 100644 tests/osaka/conftest.py diff --git a/pyproject.toml b/pyproject.toml index 47e4b34250..c5a0d93b6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -227,6 +227,7 @@ packages = [ "ethereum.forks.osaka.vm.precompiled_contracts", "ethereum.forks.osaka.vm.precompiled_contracts.bls12_381", "ethereum.forks.amsterdam", + "ethereum.forks.amsterdam.block_access_lists", "ethereum.forks.amsterdam.utils", "ethereum.forks.amsterdam.vm", "ethereum.forks.amsterdam.vm.instructions", @@ -513,6 +514,9 @@ ignore = [ "src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py" = [ "N815" # The traces must use camel case in JSON property names ] +"src/ethereum/amsterdam/blocks.py" = [ + "E501" # Line too long - needed for long ref links +] [tool.ruff.lint.mccabe] # Set the maximum allowed cyclomatic complexity. C901 default is 10. diff --git a/src/ethereum/forks/amsterdam/block_access_lists/__init__.py b/src/ethereum/amsterdam/block_access_lists/__init__.py similarity index 93% rename from src/ethereum/forks/amsterdam/block_access_lists/__init__.py rename to src/ethereum/amsterdam/block_access_lists/__init__.py index ccd762d757..3155ea77f3 100644 --- a/src/ethereum/forks/amsterdam/block_access_lists/__init__.py +++ b/src/ethereum/amsterdam/block_access_lists/__init__.py @@ -1,5 +1,5 @@ """ -Block Access Lists (EIP-7928) implementation for Ethereum Osaka fork. +Block Access Lists (EIP-7928) implementation for Ethereum Amsterdam fork. """ from .builder import ( @@ -12,6 +12,11 @@ add_touched_account, build, ) +from .rlp_utils import ( + compute_block_access_list_hash, + rlp_encode_block_access_list, + validate_block_access_list_against_execution, +) from .tracker import ( StateChangeTracker, set_transaction_index, @@ -22,11 +27,6 @@ track_storage_read, track_storage_write, ) -from .rlp_utils import ( - compute_block_access_list_hash, - rlp_encode_block_access_list, - validate_block_access_list_against_execution, -) __all__ = [ "BlockAccessListBuilder", @@ -48,4 +48,4 @@ "track_storage_read", "track_storage_write", "validate_block_access_list_against_execution", -] \ No newline at end of file +] diff --git a/src/ethereum/forks/amsterdam/block_access_lists/builder.py b/src/ethereum/amsterdam/block_access_lists/builder.py similarity index 67% rename from src/ethereum/forks/amsterdam/block_access_lists/builder.py rename to src/ethereum/amsterdam/block_access_lists/builder.py index 12dac71d5c..90ccb58807 100644 --- a/src/ethereum/forks/amsterdam/block_access_lists/builder.py +++ b/src/ethereum/amsterdam/block_access_lists/builder.py @@ -13,21 +13,21 @@ 2. **Build Phase**: After block execution, the accumulated data is sorted and encoded into the final deterministic format. -[`BlockAccessList`]: ref:ethereum.osaka.ssz_types.BlockAccessList +[`BlockAccessList`]: ref:ethereum.amsterdam.rlp_types.BlockAccessList """ from dataclasses import dataclass, field from typing import Dict, List, Set -from ethereum_types.bytes import Bytes -from ethereum_types.numeric import U32, U64, U256, Uint +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U64, U256 from ..fork_types import Address from ..rlp_types import ( AccountChanges, BalanceChange, - BlockAccessList, BlockAccessIndex, + BlockAccessList, CodeChange, NonceChange, SlotChanges, @@ -39,32 +39,35 @@ 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[Bytes, List[StorageChange]] = field(default_factory=dict) + + storage_changes: Dict[Bytes32, 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[Bytes] = field(default_factory=set) + + storage_reads: Set[Bytes32] = 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, @@ -77,14 +80,15 @@ 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. - - [`BlockAccessList`]: ref:ethereum.osaka.ssz_types.BlockAccessList + + [`BlockAccessList`]: ref:ethereum.amsterdam.rlp_types.BlockAccessList """ + accounts: Dict[Address, AccountData] = field(default_factory=dict) """ Mapping from account address to its tracked changes during block execution. @@ -94,19 +98,20 @@ class BlockAccessListBuilder: def ensure_account(builder: BlockAccessListBuilder, address: Address) -> None: """ Ensure an account exists in the builder's tracking structure. - + Creates an empty [`AccountData`] 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. - + Parameters ---------- builder : The block access list builder instance. address : The account address to ensure exists. - - [`AccountData`]: ref:ethereum.osaka.block_access_lists.builder.AccountData + + [`AccountData`] : + ref:ethereum.amsterdam.block_access_lists.builder.AccountData """ if address not in builder.accounts: builder.accounts[address] = AccountData() @@ -114,18 +119,18 @@ def ensure_account(builder: BlockAccessListBuilder, address: Address) -> None: def add_storage_write( builder: BlockAccessListBuilder, - address: Address, - slot: Bytes, - block_access_index: BlockAccessIndex, - new_value: Bytes + address: Address, + slot: Bytes32, + block_access_index: BlockAccessIndex, + new_value: Bytes32, ) -> 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. Multiple writes to the same slot are tracked separately, maintaining the order and transaction index of each change. - + Parameters ---------- builder : @@ -135,31 +140,32 @@ def add_storage_write( slot : The storage slot being written to. block_access_index : - The block access index for this change (0 for pre-execution, 1..n for transactions, n+1 for post-execution). + The block access index for this change (0 for pre-execution, + 1..n for transactions, n+1 for post-execution). new_value : The new value being written to the storage slot. """ ensure_account(builder, address) - + if slot not in builder.accounts[address].storage_changes: builder.accounts[address].storage_changes[slot] = [] - - change = StorageChange(block_access_index=block_access_index, new_value=new_value) + + 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: Bytes + builder: BlockAccessListBuilder, address: Address, slot: Bytes32 ) -> 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]. - + Parameters ---------- builder : @@ -168,7 +174,7 @@ def add_storage_read( The account address whose storage is being read. slot : The storage slot being read. - + [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 """ ensure_account(builder, address) @@ -177,17 +183,17 @@ def add_storage_read( def add_balance_change( builder: BlockAccessListBuilder, - address: Address, - block_access_index: BlockAccessIndex, - post_balance: U256 + 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. - + Parameters ---------- builder : @@ -195,29 +201,48 @@ def add_balance_change( address : The account address whose balance changed. block_access_index : - The block access index for this change (0 for pre-execution, 1..n for transactions, n+1 for post-execution). + The block access index for this change (0 for pre-execution, + 1..n for transactions, n+1 for post-execution). post_balance : The account balance after the change as U256. """ ensure_account(builder, address) - - change = BalanceChange(block_access_index=block_access_index, post_balance=post_balance) + + # 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 + 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. - + Parameters ---------- builder : @@ -225,32 +250,47 @@ def add_nonce_change( address : The account address whose nonce changed. block_access_index : - The block access index for this change (0 for pre-execution, 1..n for transactions, n+1 for post-execution). + The block access index for this change (0 for pre-execution, + 1..n for transactions, n+1 for post-execution). new_nonce : The new nonce value after the change. - - [`CREATE`]: ref:ethereum.osaka.vm.instructions.system.create - [`CREATE2`]: ref:ethereum.osaka.vm.instructions.system.create2 + + [`CREATE`]: ref:ethereum.amsterdam.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.amsterdam.vm.instructions.system.create2 """ ensure_account(builder, address) - - change = NonceChange(block_access_index=block_access_index, new_nonce=new_nonce) + + # Check if we already have a nonce change for this tx_index and update it + # This ensures we only track the final 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: + # Update the existing nonce change with the 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 + 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 [`SETCODE`] operations. - + Parameters ---------- builder : @@ -258,40 +298,48 @@ def add_code_change( address : The account address receiving new code. block_access_index : - The block access index for this change (0 for pre-execution, 1..n for transactions, n+1 for post-execution). + The block access index for this change (0 for pre-execution, + 1..n for transactions, n+1 for post-execution). new_code : The deployed contract bytecode. - - [`CREATE`]: ref:ethereum.osaka.vm.instructions.system.create - [`CREATE2`]: ref:ethereum.osaka.vm.instructions.system.create2 - [`SETCODE`]: ref:ethereum.osaka.vm.instructions.system.setcode + + [`CREATE`]: ref:ethereum.amsterdam.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.amsterdam.vm.instructions.system.create2 """ ensure_account(builder, address) - - change = CodeChange(block_access_index=block_access_index, new_code=new_code) + + 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: +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. - + Parameters ---------- builder : The block access list builder instance. address : The account address that was accessed. - - [`EXTCODEHASH`]: ref:ethereum.osaka.vm.instructions.environment.extcodehash - [`BALANCE`]: ref:ethereum.osaka.vm.instructions.environment.balance - [`EXTCODESIZE`]: ref:ethereum.osaka.vm.instructions.environment.extcodesize - [`EXTCODECOPY`]: ref:ethereum.osaka.vm.instructions.environment.extcodecopy + + [`EXTCODEHASH`] : + ref:ethereum.amsterdam.vm.instructions.environment.extcodehash + [`BALANCE`] : + ref:ethereum.amsterdam.vm.instructions.environment.balance + [`EXTCODESIZE`] : + ref:ethereum.amsterdam.vm.instructions.environment.extcodesize + [`EXTCODECOPY`] : + ref:ethereum.amsterdam.vm.instructions.environment.extcodecopy """ ensure_account(builder, address) @@ -299,58 +347,68 @@ def add_touched_account(builder: BlockAccessListBuilder, address: Address) -> No def build(builder: BlockAccessListBuilder) -> BlockAccessList: """ Build the final [`BlockAccessList`] from accumulated changes. - + 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 - + Parameters ---------- builder : The block access list builder containing all tracked changes. - + Returns ------- block_access_list : The final sorted and encoded block access list. - - [`BlockAccessList`]: ref:ethereum.osaka.ssz_types.BlockAccessList + + [`BlockAccessList`]: ref:ethereum.amsterdam.rlp_types.BlockAccessList """ account_changes_list = [] - + 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)) - + 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)) - + + 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 + code_changes=code_changes, ) - + account_changes_list.append(account_change) - + account_changes_list.sort(key=lambda x: x.address) - - return BlockAccessList(account_changes=tuple(account_changes_list)) \ No newline at end of file + + return BlockAccessList(account_changes=tuple(account_changes_list)) diff --git a/src/ethereum/amsterdam/block_access_lists/rlp_utils.py b/src/ethereum/amsterdam/block_access_lists/rlp_utils.py new file mode 100644 index 0000000000..c26e4a5e2b --- /dev/null +++ b/src/ethereum/amsterdam/block_access_lists/rlp_utils.py @@ -0,0 +1,230 @@ +""" +Block Access List RLP Utilities for EIP-7928 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Utilities for working with Block Access Lists using RLP encoding, +as specified in EIP-7928. + +This module provides: + +- RLP encoding functions for all Block Access List types +- Hash computation using [`keccak256`] +- Validation logic to ensure structural correctness + +The encoding follows the RLP specification used throughout Ethereum. + +[`keccak256`]: ref:ethereum.crypto.hash.keccak256 +""" + +from typing import cast + +from ethereum_rlp import Extended, rlp +from ethereum_types.bytes import Bytes +from ethereum_types.numeric import Uint + +from ethereum.crypto.hash import Hash32, keccak256 + +from ..rlp_types import MAX_CODE_SIZE, MAX_TXS, BlockAccessList +from .builder import BlockAccessListBuilder + + +def compute_block_access_list_hash( + block_access_list: BlockAccessList, +) -> Hash32: + """ + Compute the hash of a Block Access List. + + The Block Access List is RLP-encoded and then hashed with keccak256. + + Parameters + ---------- + block_access_list : + The Block Access List to hash. + + Returns + ------- + hash : + The keccak256 hash of the RLP-encoded Block Access List. + """ + block_access_list_bytes = rlp_encode_block_access_list(block_access_list) + return keccak256(block_access_list_bytes) + + +def rlp_encode_block_access_list(block_access_list: BlockAccessList) -> Bytes: + """ + Encode a [`BlockAccessList`] to RLP bytes. + + This is the top-level encoding function that produces the final RLP + representation of a block's access list, following the updated EIP-7928 + specification. + + Parameters + ---------- + block_access_list : + The block access list to encode. + + Returns + ------- + encoded : + The complete RLP-encoded block access list. + + [`BlockAccessList`]: ref:ethereum.amsterdam.rlp_types.BlockAccessList + """ + # Encode as a list of AccountChanges directly (not wrapped) + account_changes_list = [] + for account in block_access_list.account_changes: + # Each account is encoded as: + # [address, storage_changes, storage_reads, + # balance_changes, nonce_changes, code_changes] + storage_changes_list = [ + [ + slot_changes.slot, + [ + [Uint(c.block_access_index), c.new_value] + for c in slot_changes.changes + ], + ] + for slot_changes in account.storage_changes + ] + + storage_reads_list = list(account.storage_reads) + + balance_changes_list = [ + [Uint(bc.block_access_index), Uint(bc.post_balance)] + for bc in account.balance_changes + ] + + nonce_changes_list = [ + [Uint(nc.block_access_index), Uint(nc.new_nonce)] + for nc in account.nonce_changes + ] + + code_changes_list = [ + [Uint(cc.block_access_index), cc.new_code] + for cc in account.code_changes + ] + + account_changes_list.append( + [ + account.address, + storage_changes_list, + storage_reads_list, + balance_changes_list, + nonce_changes_list, + code_changes_list, + ] + ) + + encoded = rlp.encode(cast(Extended, account_changes_list)) + return Bytes(encoded) + + +def validate_block_access_list_against_execution( + block_access_list: BlockAccessList, + block_access_list_builder: BlockAccessListBuilder | None = None, +) -> bool: + """ + Validate that a Block Access List is structurally correct and + optionally matches a builder's state. + + Parameters + ---------- + block_access_list : + The Block Access List to validate. + block_access_list_builder : + Optional Block Access List builder to validate against. + If provided, checks that the + Block Access List hash matches what would be built from + the builder's current state. + + Returns + ------- + valid : + True if the Block Access List is structurally valid and + matches the builder (if provided). + """ + # 1. Validate structural constraints + + # Check that storage changes and reads don't overlap for the same slot + for account in block_access_list.account_changes: + changed_slots = {sc.slot for sc in account.storage_changes} + read_slots = set(account.storage_reads) + + # A slot should not be in both changes and reads (per EIP-7928) + if changed_slots & read_slots: + return False + + # 2. Validate ordering (addresses should be sorted lexicographically) + addresses = [ + account.address for account in block_access_list.account_changes + ] + if addresses != sorted(addresses): + return False + + # 3. Validate all data is within bounds + max_block_access_index = ( + MAX_TXS + 1 + ) # 0 for pre-exec, 1..MAX_TXS for txs, MAX_TXS+1 for post-exec + for account in block_access_list.account_changes: + # Validate storage slots are sorted within each account + storage_slots = [sc.slot for sc in account.storage_changes] + if storage_slots != sorted(storage_slots): + return False + + # Check storage changes + for slot_changes in account.storage_changes: + # Check changes are sorted by block_access_index + indices = [c.block_access_index for c in slot_changes.changes] + if indices != sorted(indices): + return False + + for change in slot_changes.changes: + if int(change.block_access_index) > max_block_access_index: + return False + + # Check balance changes are sorted by block_access_index + balance_indices = [ + bc.block_access_index for bc in account.balance_changes + ] + if balance_indices != sorted(balance_indices): + return False + + for balance_change in account.balance_changes: + if int(balance_change.block_access_index) > max_block_access_index: + return False + + # Check nonce changes are sorted by block_access_index + nonce_indices = [nc.block_access_index for nc in account.nonce_changes] + if nonce_indices != sorted(nonce_indices): + return False + + for nonce_change in account.nonce_changes: + if int(nonce_change.block_access_index) > max_block_access_index: + return False + + # Check code changes are sorted by block_access_index + code_indices = [cc.block_access_index for cc in account.code_changes] + if code_indices != sorted(code_indices): + return False + + for code_change in account.code_changes: + if int(code_change.block_access_index) > max_block_access_index: + return False + if len(code_change.new_code) > MAX_CODE_SIZE: + return False + + # 4. If Block Access List builder provided, validate against it + # by comparing hashes + if block_access_list_builder is not None: + from .builder import build + + # Build a Block Access List from the builder + expected_block_access_list = build(block_access_list_builder) + + # Compare hashes + if compute_block_access_list_hash( + block_access_list + ) != compute_block_access_list_hash(expected_block_access_list): + return False + + return True diff --git a/src/ethereum/forks/amsterdam/block_access_lists/tracker.py b/src/ethereum/amsterdam/block_access_lists/tracker.py similarity index 75% rename from src/ethereum/forks/amsterdam/block_access_lists/tracker.py rename to src/ethereum/amsterdam/block_access_lists/tracker.py index 4ae758db8f..34f7aa95f0 100644 --- a/src/ethereum/forks/amsterdam/block_access_lists/tracker.py +++ b/src/ethereum/amsterdam/block_access_lists/tracker.py @@ -16,15 +16,13 @@ """ from dataclasses import dataclass, field -from typing import Dict, Optional +from typing import TYPE_CHECKING, Dict -from ethereum_types.bytes import Bytes -from ethereum_types.numeric import U32, U64, U256, Uint +from ethereum_types.bytes import Bytes, Bytes32 +from ethereum_types.numeric import U64, U256, Uint +from ..fork_types import Address from ..rlp_types import BlockAccessIndex - -from ..fork_types import Address, Account -from ..state import State, get_account, get_storage from .builder import ( BlockAccessListBuilder, add_balance_change, @@ -35,68 +33,79 @@ add_touched_account, ) +if TYPE_CHECKING: + from ..state import State # noqa: F401 + @dataclass class StateChangeTracker: """ Tracks state changes during transaction execution for Block Access List construction. - + This tracker maintains a cache of pre-state values and coordinates with the [`BlockAccessListBuilder`] to record all state changes made during block execution. It ensures that only actual changes (not no-op writes) are recorded in the access list. - - [`BlockAccessListBuilder`]: ref:ethereum.osaka.block_access_lists.builder.BlockAccessListBuilder + + [`BlockAccessListBuilder`]: + ref:ethereum.amsterdam.block_access_lists.builder.BlockAccessListBuilder """ + block_access_list_builder: BlockAccessListBuilder """ The builder instance that accumulates all tracked changes. """ - + pre_storage_cache: Dict[tuple, U256] = field(default_factory=dict) """ - Cache of pre-state storage values, keyed by (address, slot) tuples. - This cache persists across transactions within a block to track the - original state before any modifications. + Cache of pre-transaction storage values, keyed by (address, slot) tuples. + This cache is cleared at the start of each transaction to track values + from the beginning of the current transaction. """ - + current_block_access_index: int = 0 """ - The current block access index (0 for pre-execution, 1..n for transactions, n+1 for post-execution). + The current block access index (0 for pre-execution, + 1..n for transactions, n+1 for post-execution). """ -def set_transaction_index(tracker: StateChangeTracker, block_access_index: int) -> None: +def set_transaction_index( + tracker: StateChangeTracker, block_access_index: int +) -> None: """ Set the current block access index for tracking changes. - - Must be called before processing each transaction/system contract to ensure changes + + Must be called before processing each transaction/system contract + to ensure changes are associated with the correct block access index. - + Parameters ---------- tracker : The state change tracker instance. block_access_index : - The block access index (0 for pre-execution, 1..n for transactions, n+1 for post-execution). + The block access index (0 for pre-execution, + 1..n for transactions, n+1 for post-execution). """ tracker.current_block_access_index = block_access_index + # Clear the pre-storage cache for each new transaction to ensure + # no-op writes are detected relative to the transaction start + tracker.pre_storage_cache.clear() def capture_pre_state( - tracker: StateChangeTracker, - address: Address, - key: Bytes, - state: State + tracker: StateChangeTracker, address: Address, key: Bytes32, state: "State" ) -> U256: """ - Capture and cache the pre-state value for a storage location. - - Retrieves the storage value from before any transactions in the current - block modified it. The value is cached to avoid repeated lookups and - to maintain consistency across multiple accesses. - + Capture and cache the pre-transaction value for a storage location. + + Retrieves the storage value from the beginning of the current transaction. + The value is cached within the transaction to avoid repeated lookups and + to maintain consistency across multiple accesses within the same + transaction. + Parameters ---------- tracker : @@ -107,25 +116,30 @@ def capture_pre_state( The storage slot to read. state : The current execution state. - + Returns ------- value : - The original storage value before any block modifications. + The storage value at the beginning of the current transaction. """ cache_key = (address, key) if cache_key not in tracker.pre_storage_cache: + # Import locally to avoid circular import + from ..state import get_storage + tracker.pre_storage_cache[cache_key] = get_storage(state, address, key) return tracker.pre_storage_cache[cache_key] -def track_address_access(tracker: StateChangeTracker, address: Address) -> None: +def track_address_access( + tracker: StateChangeTracker, address: Address +) -> None: """ Track that an address was accessed. - + Records account access even when no state changes occur. This is important for operations that read account data without modifying it. - + Parameters ---------- tracker : @@ -137,18 +151,15 @@ def track_address_access(tracker: StateChangeTracker, address: Address) -> None: def track_storage_read( - tracker: StateChangeTracker, - address: Address, - key: Bytes, - state: State + tracker: StateChangeTracker, address: Address, key: Bytes32, state: "State" ) -> None: """ Track a storage read operation. - + Records that a storage slot was read and captures its pre-state value. The slot will only appear in the final access list if it wasn't also written to during block execution. - + Parameters ---------- tracker : @@ -161,26 +172,26 @@ def track_storage_read( The current execution state. """ track_address_access(tracker, address) - + capture_pre_state(tracker, address, key, state) - + add_storage_read(tracker.block_access_list_builder, address, key) def track_storage_write( tracker: StateChangeTracker, - address: Address, - key: Bytes, - new_value: U256, - state: State + address: Address, + key: Bytes32, + new_value: U256, + state: "State", ) -> None: """ Track a storage write operation. - + Records storage modifications, but only if the new value differs from the pre-state value. No-op writes (where the value doesn't change) are tracked as reads instead, as specified in [EIP-7928]. - + Parameters ---------- tracker : @@ -193,22 +204,22 @@ def track_storage_write( The new value to write. state : The current execution state. - + [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 """ track_address_access(tracker, address) - + pre_value = capture_pre_state(tracker, address, key, state) - + value_bytes = new_value.to_be_bytes32() - + if pre_value != new_value: add_storage_write( tracker.block_access_list_builder, address, key, BlockAccessIndex(tracker.current_block_access_index), - value_bytes + value_bytes, ) else: add_storage_read(tracker.block_access_list_builder, address, key) @@ -216,18 +227,17 @@ def track_storage_write( def track_balance_change( tracker: StateChangeTracker, - address: Address, - new_balance: U256, - state: State + address: Address, + new_balance: U256, ) -> None: """ Track a balance change for an account. - + Records the new balance after any balance-affecting operation, including transfers, gas payments, block rewards, and withdrawals. The balance is encoded as a 16-byte value (uint128) which is sufficient for the total ETH supply. - + Parameters ---------- tracker : @@ -236,32 +246,27 @@ def track_balance_change( The account address whose balance changed. new_balance : The new balance value. - state : - The current execution state. """ track_address_access(tracker, address) - + add_balance_change( tracker.block_access_list_builder, address, BlockAccessIndex(tracker.current_block_access_index), - new_balance + new_balance, ) def track_nonce_change( - tracker: StateChangeTracker, - address: Address, - new_nonce: Uint, - state: State + tracker: StateChangeTracker, address: Address, new_nonce: Uint ) -> None: """ Track a nonce change for an account. - + Records nonce increments for both EOAs (when sending transactions) and contracts (when performing [`CREATE`] or [`CREATE2`] operations). Deployed contracts also have their initial nonce tracked. - + Parameters ---------- tracker : @@ -272,32 +277,29 @@ def track_nonce_change( The new nonce value. state : The current execution state. - - [`CREATE`]: ref:ethereum.osaka.vm.instructions.system.create - [`CREATE2`]: ref:ethereum.osaka.vm.instructions.system.create2 + + [`CREATE`]: ref:ethereum.amsterdam.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.amsterdam.vm.instructions.system.create2 """ track_address_access(tracker, address) add_nonce_change( tracker.block_access_list_builder, address, BlockAccessIndex(tracker.current_block_access_index), - U64(new_nonce) + U64(new_nonce), ) def track_code_change( - tracker: StateChangeTracker, - address: Address, - new_code: Bytes, - state: State + tracker: StateChangeTracker, address: Address, new_code: Bytes ) -> None: """ Track a code change for contract deployment. - + Records new contract code deployments via [`CREATE`], [`CREATE2`], or [`SETCODE`] operations. This function is called when contract bytecode is deployed to an address. - + Parameters ---------- tracker : @@ -306,33 +308,29 @@ def track_code_change( The address receiving the contract code. new_code : The deployed contract bytecode. - state : - The current execution state. - - [`CREATE`]: ref:ethereum.osaka.vm.instructions.system.create - [`CREATE2`]: ref:ethereum.osaka.vm.instructions.system.create2 - [`SETCODE`]: ref:ethereum.osaka.vm.instructions.system.setcode + + [`CREATE`]: ref:ethereum.amsterdam.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.amsterdam.vm.instructions.system.create2 """ track_address_access(tracker, address) add_code_change( tracker.block_access_list_builder, address, BlockAccessIndex(tracker.current_block_access_index), - new_code + new_code, ) def finalize_transaction_changes( - tracker: StateChangeTracker, - state: State + tracker: StateChangeTracker, state: "State" ) -> None: """ Finalize changes for the current transaction. - + This method is called at the end of each transaction execution. Currently a no-op as all tracking is done incrementally during execution, but provided for future extensibility. - + Parameters ---------- tracker : @@ -340,4 +338,4 @@ def finalize_transaction_changes( state : The current execution state. """ - pass \ No newline at end of file + pass diff --git a/src/ethereum/forks/amsterdam/rlp_types.py b/src/ethereum/amsterdam/rlp_types.py similarity index 75% rename from src/ethereum/forks/amsterdam/rlp_types.py rename to src/ethereum/amsterdam/rlp_types.py index c87577ce76..79c37ca215 100644 --- a/src/ethereum/forks/amsterdam/rlp_types.py +++ b/src/ethereum/amsterdam/rlp_types.py @@ -6,11 +6,12 @@ as specified in EIP-7928. These structures enable efficient encoding and decoding of all accounts and storage locations accessed during block execution. -The encoding follows the pattern: address -> field -> block_access_index -> change +The encoding follows the pattern: +address -> field -> block_access_index -> change """ from dataclasses import dataclass -from typing import List, Tuple +from typing import Tuple from ethereum_types.bytes import Bytes, Bytes20, Bytes32 from ethereum_types.frozen import slotted_freezable @@ -27,8 +28,8 @@ # Constants chosen to support a 630m block gas limit MAX_TXS = 30_000 -MAX_SLOTS = 300_000 -MAX_ACCOUNTS = 300_000 +# MAX_SLOTS = 300_000 +# MAX_ACCOUNTS = 300_000 MAX_CODE_SIZE = 24_576 MAX_CODE_CHANGES = 1 @@ -40,6 +41,7 @@ class StorageChange: Storage change: [block_access_index, new_value] RLP encoded as a list """ + block_access_index: BlockAccessIndex new_value: StorageValue @@ -51,6 +53,7 @@ class BalanceChange: Balance change: [block_access_index, post_balance] RLP encoded as a list """ + block_access_index: BlockAccessIndex post_balance: Balance @@ -62,6 +65,7 @@ class NonceChange: Nonce change: [block_access_index, new_nonce] RLP encoded as a list """ + block_access_index: BlockAccessIndex new_nonce: Nonce @@ -73,6 +77,7 @@ class CodeChange: Code change: [block_access_index, new_code] RLP encoded as a list """ + block_access_index: BlockAccessIndex new_code: CodeData @@ -84,6 +89,7 @@ class SlotChanges: All changes to a single storage slot: [slot, [changes]] RLP encoded as a list """ + slot: StorageKey changes: Tuple[StorageChange, ...] @@ -93,14 +99,26 @@ class SlotChanges: class AccountChanges: """ All changes for a single account, grouped by field type. - RLP encoded as: [address, storage_changes, storage_reads, balance_changes, nonce_changes, code_changes] + RLP encoded as: [address, storage_changes, storage_reads, + balance_changes, nonce_changes, code_changes] """ + address: Address - storage_changes: Tuple[SlotChanges, ...] # slot -> [block_access_index -> new_value] - storage_reads: Tuple[StorageKey, ...] # read-only storage keys - balance_changes: Tuple[BalanceChange, ...] # [block_access_index -> post_balance] - nonce_changes: Tuple[NonceChange, ...] # [block_access_index -> new_nonce] - code_changes: Tuple[CodeChange, ...] # [block_access_index -> new_code] + + # slot -> [block_access_index -> new_value] + storage_changes: Tuple[SlotChanges, ...] + + # read-only storage keys + storage_reads: Tuple[StorageKey, ...] + + # [block_access_index -> post_balance] + balance_changes: Tuple[BalanceChange, ...] + + # [block_access_index -> new_nonce] + nonce_changes: Tuple[NonceChange, ...] + + # [block_access_index -> new_code] + code_changes: Tuple[CodeChange, ...] @slotted_freezable @@ -111,4 +129,5 @@ class BlockAccessList: Contains all addresses accessed during block execution. RLP encoded as a list of AccountChanges """ - account_changes: Tuple[AccountChanges, ...] \ No newline at end of file + + account_changes: Tuple[AccountChanges, ...] diff --git a/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py b/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py deleted file mode 100644 index 335e4d1c42..0000000000 --- a/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py +++ /dev/null @@ -1,396 +0,0 @@ -""" -Block Access List RLP Utilities for EIP-7928 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Utilities for working with Block Access Lists using RLP encoding, -as specified in EIP-7928. - -This module provides: - -- RLP encoding functions for all Block Access List types -- Hash computation using [`keccak256`] -- Validation logic to ensure structural correctness - -The encoding follows the RLP specification used throughout Ethereum. - -[`keccak256`]: ref:ethereum.crypto.hash.keccak256 -""" - -from typing import Optional - -from ethereum_rlp import rlp -from ethereum_types.bytes import Bytes -from ethereum_types.numeric import Uint - -from ethereum.crypto.hash import Hash32, keccak256 - -from ..rlp_types import ( - BlockAccessList, - AccountChanges, - SlotChanges, - StorageChange, - BalanceChange, - NonceChange, - CodeChange, - MAX_TXS, - MAX_SLOTS, - MAX_ACCOUNTS, - MAX_CODE_SIZE, -) - - -def compute_block_access_list_hash(block_access_list: BlockAccessList) -> Hash32: - """ - Compute the hash of a Block Access List. - - The Block Access List is RLP-encoded and then hashed with keccak256. - - Parameters - ---------- - block_access_list : - The Block Access List to hash. - - Returns - ------- - hash : - The keccak256 hash of the RLP-encoded Block Access List. - """ - block_access_list_bytes = rlp_encode_block_access_list(block_access_list) - return keccak256(block_access_list_bytes) - - -def rlp_encode_storage_change(change: StorageChange) -> bytes: - """ - Encode a [`StorageChange`] as RLP. - - Encoded as: [block_access_index, new_value] - - Parameters - ---------- - change : - The storage change to encode. - - Returns - ------- - encoded : - The RLP-encoded storage change. - - [`StorageChange`]: ref:ethereum.osaka.rlp_types.StorageChange - """ - return rlp.encode([ - Uint(change.block_access_index), - change.new_value - ]) - - -def rlp_encode_balance_change(change: BalanceChange) -> bytes: - """ - Encode a [`BalanceChange`] as RLP. - - Encoded as: [block_access_index, post_balance] - - Parameters - ---------- - change : - The balance change to encode. - - Returns - ------- - encoded : - The RLP-encoded balance change. - - [`BalanceChange`]: ref:ethereum.osaka.rlp_types.BalanceChange - """ - return rlp.encode([ - Uint(change.block_access_index), - change.post_balance - ]) - - -def rlp_encode_nonce_change(change: NonceChange) -> bytes: - """ - Encode a [`NonceChange`] as RLP. - - Encoded as: [block_access_index, new_nonce] - - Parameters - ---------- - change : - The nonce change to encode. - - Returns - ------- - encoded : - The RLP-encoded nonce change. - - [`NonceChange`]: ref:ethereum.osaka.rlp_types.NonceChange - """ - return rlp.encode([ - Uint(change.block_access_index), - Uint(change.new_nonce) - ]) - - -def rlp_encode_code_change(change: CodeChange) -> bytes: - """ - Encode a [`CodeChange`] as RLP. - - Encoded as: [block_access_index, new_code] - - Parameters - ---------- - change : - The code change to encode. - - Returns - ------- - encoded : - The RLP-encoded code change. - - [`CodeChange`]: ref:ethereum.osaka.rlp_types.CodeChange - """ - return rlp.encode([ - Uint(change.block_access_index), - change.new_code - ]) - - -def rlp_encode_slot_changes(slot_changes: SlotChanges) -> bytes: - """ - Encode [`SlotChanges`] as RLP. - - Encoded as: [slot, [changes]] - - Parameters - ---------- - slot_changes : - The slot changes to encode. - - Returns - ------- - encoded : - The RLP-encoded slot changes. - - [`SlotChanges`]: ref:ethereum.osaka.rlp_types.SlotChanges - """ - # Encode each change as [block_access_index, new_value] - changes_list = [ - [Uint(change.block_access_index), change.new_value] - for change in slot_changes.changes - ] - - return rlp.encode([ - slot_changes.slot, - changes_list - ]) - - -def rlp_encode_account_changes(account: AccountChanges) -> bytes: - """ - Encode [`AccountChanges`] as RLP. - - Encoded as: [address, storage_changes, storage_reads, balance_changes, nonce_changes, code_changes] - - Parameters - ---------- - account : - The account changes to encode. - - Returns - ------- - encoded : - The RLP-encoded account changes. - - [`AccountChanges`]: ref:ethereum.osaka.rlp_types.AccountChanges - """ - # Encode storage_changes: [[slot, [[block_access_index, new_value], ...]], ...] - storage_changes_list = [ - [slot_changes.slot, [[Uint(c.block_access_index), c.new_value] for c in slot_changes.changes]] - for slot_changes in account.storage_changes - ] - - # Encode storage_reads: [slot1, slot2, ...] - storage_reads_list = list(account.storage_reads) - - # Encode balance_changes: [[block_access_index, post_balance], ...] - balance_changes_list = [ - [Uint(bc.block_access_index), bc.post_balance] - for bc in account.balance_changes - ] - - # Encode nonce_changes: [[block_access_index, new_nonce], ...] - nonce_changes_list = [ - [Uint(nc.block_access_index), Uint(nc.new_nonce)] - for nc in account.nonce_changes - ] - - # Encode code_changes: [[block_access_index, new_code], ...] - code_changes_list = [ - [Uint(cc.block_access_index), cc.new_code] - for cc in account.code_changes - ] - - return rlp.encode([ - account.address, - storage_changes_list, - storage_reads_list, - balance_changes_list, - nonce_changes_list, - code_changes_list - ]) - - -def rlp_encode_block_access_list(block_access_list: BlockAccessList) -> Bytes: - """ - Encode a [`BlockAccessList`] to RLP bytes. - - This function produces the final RLP representation of a block's access list, - following the EIP-7928 specification. - - Parameters - ---------- - block_access_list : - The block access list to encode. - - Returns - ------- - encoded : - The complete RLP-encoded block access list. - - [`BlockAccessList`]: ref:ethereum.osaka.rlp_types.BlockAccessList - """ - # Encode as a list of AccountChanges directly (not wrapped) - account_changes_list = [] - for account in block_access_list.account_changes: - # Each account is encoded as: - # [address, storage_changes, storage_reads, balance_changes, nonce_changes, code_changes] - storage_changes_list = [ - [slot_changes.slot, [[Uint(c.block_access_index), c.new_value] for c in slot_changes.changes]] - for slot_changes in account.storage_changes - ] - - storage_reads_list = list(account.storage_reads) - - balance_changes_list = [ - [Uint(bc.block_access_index), bc.post_balance] - for bc in account.balance_changes - ] - - nonce_changes_list = [ - [Uint(nc.block_access_index), Uint(nc.new_nonce)] - for nc in account.nonce_changes - ] - - code_changes_list = [ - [Uint(cc.block_access_index), cc.new_code] - for cc in account.code_changes - ] - - account_changes_list.append([ - account.address, - storage_changes_list, - storage_reads_list, - balance_changes_list, - nonce_changes_list, - code_changes_list - ]) - - encoded = rlp.encode(account_changes_list) - return Bytes(encoded) - - -def validate_block_access_list_against_execution( - block_access_list: BlockAccessList, - block_access_list_builder: Optional['BlockAccessListBuilder'] = None -) -> bool: - """ - Validate that a Block Access List is structurally correct and optionally matches a builder's state. - - Parameters - ---------- - block_access_list : - The Block Access List to validate. - block_access_list_builder : - Optional Block Access List builder to validate against. If provided, checks that the - Block Access List hash matches what would be built from the builder's current state. - - Returns - ------- - valid : - True if the Block Access List is structurally valid and matches the builder (if provided). - """ - # 1. Validate structural constraints - - # Check that storage changes and reads don't overlap for the same slot - for account in block_access_list.account_changes: - changed_slots = {sc.slot for sc in account.storage_changes} - read_slots = set(account.storage_reads) - - # A slot should not be in both changes and reads (per EIP-7928) - if changed_slots & read_slots: - return False - - # 2. Validate ordering (addresses should be sorted lexicographically) - addresses = [account.address for account in block_access_list.account_changes] - if addresses != sorted(addresses): - return False - - # 3. Validate all data is within bounds - max_block_access_index = MAX_TXS + 1 # 0 for pre-exec, 1..MAX_TXS for txs, MAX_TXS+1 for post-exec - for account in block_access_list.account_changes: - # Validate storage slots are sorted within each account - storage_slots = [sc.slot for sc in account.storage_changes] - if storage_slots != sorted(storage_slots): - return False - - # Check storage changes - for slot_changes in account.storage_changes: - # Check changes are sorted by block_access_index - indices = [c.block_access_index for c in slot_changes.changes] - if indices != sorted(indices): - return False - - for change in slot_changes.changes: - if change.block_access_index > max_block_access_index: - return False - - # Check balance changes are sorted by block_access_index - balance_indices = [bc.block_access_index for bc in account.balance_changes] - if balance_indices != sorted(balance_indices): - return False - - for balance_change in account.balance_changes: - if balance_change.block_access_index > max_block_access_index: - return False - - # Check nonce changes are sorted by block_access_index - nonce_indices = [nc.block_access_index for nc in account.nonce_changes] - if nonce_indices != sorted(nonce_indices): - return False - - for nonce_change in account.nonce_changes: - if nonce_change.block_access_index > max_block_access_index: - return False - - # Check code changes are sorted by block_access_index - code_indices = [cc.block_access_index for cc in account.code_changes] - if code_indices != sorted(code_indices): - return False - - for code_change in account.code_changes: - if code_change.block_access_index > max_block_access_index: - return False - if len(code_change.new_code) > MAX_CODE_SIZE: - return False - - # 4. If Block Access List builder provided, validate against it by comparing hashes - if block_access_list_builder is not None: - from .builder import build - # Build a Block Access List from the builder - expected_block_access_list = build(block_access_list_builder) - - # Compare hashes - if compute_block_access_list_hash(block_access_list) != compute_block_access_list_hash(expected_block_access_list): - return False - - return True \ No newline at end of file diff --git a/src/ethereum/forks/amsterdam/block_access_lists/utils.py b/src/ethereum/forks/amsterdam/block_access_lists/utils.py deleted file mode 100644 index a38ebc43b1..0000000000 --- a/src/ethereum/forks/amsterdam/block_access_lists/utils.py +++ /dev/null @@ -1,521 +0,0 @@ -""" -Block Access List Utilities for EIP-7928 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Utilities for working with Block Access Lists, including SSZ encoding, -hashing, and validation functions. - -This module provides: - -- SSZ encoding functions for all Block Access List types -- Hash computation using [`keccak256`] -- Validation logic to ensure structural correctness - -The encoding follows the [SSZ specification] used in Ethereum consensus layer. - -[`keccak256`]: ref:ethereum.crypto.hash.keccak256 -[SSZ specification]: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md -""" - -from typing import Union, Optional -from ethereum_types.bytes import Bytes -from ethereum_types.numeric import Uint - -from ethereum.crypto.hash import Hash32, keccak256 - -from ..ssz_types import ( - BlockAccessList, - AccountChanges, - SlotChanges, - SlotRead, - StorageChange, - BalanceChange, - NonceChange, - CodeChange, - MAX_TRANSACTIONS, - MAX_SLOTS, - MAX_ACCOUNTS, - MAX_CODE_SIZE, -) - - -def compute_block_access_list_hash(block_access_list: BlockAccessList) -> Hash32: - """ - Compute the hash of a Block Access List. - - The Block Access List is SSZ-encoded and then hashed with keccak256. - - Parameters - ---------- - block_access_list : - The Block Access List to hash. - - Returns - ------- - hash : - The keccak256 hash of the SSZ-encoded Block Access List. - """ - block_access_list_bytes = ssz_encode_block_access_list(block_access_list) - return keccak256(block_access_list_bytes) - - -def ssz_encode_uint(value: Union[int, Uint], size: int) -> bytes: - """ - Encode an unsigned integer as SSZ (little-endian). - - Parameters - ---------- - value : - The integer value to encode. - size : - The size in bytes for the encoded output. - - Returns - ------- - encoded : - The little-endian encoded bytes. - """ - if isinstance(value, Uint): - value = int(value) - return value.to_bytes(size, 'little') - - -def ssz_encode_bytes(data: bytes) -> bytes: - """ - Encode fixed-size bytes as SSZ. - - For fixed-size byte arrays, SSZ encoding is simply the bytes themselves. - - Parameters - ---------- - data : - The bytes to encode. - - Returns - ------- - encoded : - The encoded bytes (unchanged). - """ - return data - - -def ssz_encode_list(items: tuple, encode_item_fn, max_length: int = None) -> bytes: - """ - Encode a list or tuple as SSZ. - - Handles both fixed-length and variable-length lists according to the - [SSZ specification]. Variable-length lists use offset encoding when - elements have variable size. - - Parameters - ---------- - items : - The tuple of items to encode. - encode_item_fn : - Function to encode individual items. - max_length : - Maximum list length (if specified, indicates variable-length list). - - Returns - ------- - encoded : - The SSZ-encoded list. - - [SSZ specification]: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md - """ - result = bytearray() - - if max_length is None: - # Fixed-length list/tuple: just concatenate - for item in items: - result.extend(encode_item_fn(item)) - else: - # Variable-length lists use offset encoding - item_count = len(items) - if item_count == 0: - # Empty list is encoded as just the 4-byte offset pointing to itself - return ssz_encode_uint(4, 4) - - # Calculate if items are fixed or variable size - first_item_encoded = encode_item_fn(items[0]) if items else b'' - is_fixed_size = all(len(encode_item_fn(item)) == len(first_item_encoded) for item in items) - - if is_fixed_size: - # Fixed-size elements: concatenate directly - for item in items: - result.extend(encode_item_fn(item)) - else: - # Variable-size elements: use offset encoding - # Reserve space for offsets - offset_start = 4 * item_count - data_section = bytearray() - - for item in items: - # Write offset - result.extend(ssz_encode_uint(offset_start + len(data_section), 4)) - # Encode item data - item_data = encode_item_fn(item) - data_section.extend(item_data) - - result.extend(data_section) - - return bytes(result) - - -def ssz_encode_storage_change(change: StorageChange) -> bytes: - """ - Encode a [`StorageChange`] as SSZ. - - Parameters - ---------- - change : - The storage change to encode. - - Returns - ------- - encoded : - The SSZ-encoded storage change. - - [`StorageChange`]: ref:ethereum.osaka.ssz_types.StorageChange - """ - return ( - ssz_encode_uint(change.tx_index, 2) # TxIndex as uint16 - + ssz_encode_bytes(change.new_value) # StorageValue as Bytes32 - ) - - -def ssz_encode_balance_change(change: BalanceChange) -> bytes: - """ - Encode a [`BalanceChange`] as SSZ. - - Parameters - ---------- - change : - The balance change to encode. - - Returns - ------- - encoded : - The SSZ-encoded balance change. - - [`BalanceChange`]: ref:ethereum.osaka.ssz_types.BalanceChange - """ - return ( - ssz_encode_uint(change.tx_index, 2) # TxIndex as uint16 - + ssz_encode_uint(change.post_balance, 32) # Balance as uint256 - ) - - -def ssz_encode_nonce_change(change: NonceChange) -> bytes: - """ - Encode a [`NonceChange`] as SSZ. - - Parameters - ---------- - change : - The nonce change to encode. - - Returns - ------- - encoded : - The SSZ-encoded nonce change. - - [`NonceChange`]: ref:ethereum.osaka.ssz_types.NonceChange - """ - return ( - ssz_encode_uint(change.tx_index, 2) # TxIndex as uint16 - + ssz_encode_uint(change.new_nonce, 8) # Nonce as uint64 - ) - - -def ssz_encode_code_change(change: CodeChange) -> bytes: - """ - Encode a [`CodeChange`] as SSZ. - - Code changes use variable-length encoding since contract bytecode - can vary in size up to [`MAX_CODE_SIZE`]. - - Parameters - ---------- - change : - The code change to encode. - - Returns - ------- - encoded : - The SSZ-encoded code change. - - [`CodeChange`]: ref:ethereum.osaka.ssz_types.CodeChange - [`MAX_CODE_SIZE`]: ref:ethereum.osaka.ssz_types.MAX_CODE_SIZE - """ - result = bytearray() - result.extend(ssz_encode_uint(change.tx_index, 2)) # TxIndex as uint16 - # Code is variable length, so we encode length first for variable-size containers - code_len = len(change.new_code) - # In SSZ, variable-length byte arrays are prefixed with their length - result.extend(ssz_encode_uint(code_len, 4)) - result.extend(change.new_code) - return bytes(result) - - -def ssz_encode_slot_changes(slot_changes: SlotChanges) -> bytes: - """ - Encode [`SlotChanges`] as SSZ. - - Encodes a storage slot and all changes made to it during block execution. - - Parameters - ---------- - slot_changes : - The slot changes to encode. - - Returns - ------- - encoded : - The SSZ-encoded slot changes. - - [`SlotChanges`]: ref:ethereum.osaka.ssz_types.SlotChanges - """ - result = bytearray() - result.extend(ssz_encode_bytes(slot_changes.slot)) # StorageKey as Bytes32 - # Encode the list of changes - changes_encoded = ssz_encode_list( - slot_changes.changes, - ssz_encode_storage_change, - MAX_TRANSACTIONS # max length for changes - ) - result.extend(changes_encoded) - return bytes(result) - - -def ssz_encode_slot_read(slot_read: SlotRead) -> bytes: - """ - Encode a [`SlotRead`] as SSZ. - - For read-only slots, only the slot key is encoded. - - Parameters - ---------- - slot_read : - The slot read to encode. - - Returns - ------- - encoded : - The SSZ-encoded slot read. - - [`SlotRead`]: ref:ethereum.osaka.ssz_types.SlotRead - """ - return ssz_encode_bytes(slot_read.slot) # StorageKey as Bytes32 - - -def ssz_encode_account_changes(account: AccountChanges) -> bytes: - """ - Encode [`AccountChanges`] as SSZ. - - Encodes all changes for a single account using variable-size struct - encoding with offsets for the variable-length fields. - - Parameters - ---------- - account : - The account changes to encode. - - Returns - ------- - encoded : - The SSZ-encoded account changes. - - [`AccountChanges`]: ref:ethereum.osaka.ssz_types.AccountChanges - """ - # For variable-size struct, we use offset encoding - result = bytearray() - offsets = [] - data_section = bytearray() - - # Fixed-size fields first - result.extend(ssz_encode_bytes(account.address)) # Address as Bytes20 - - # Variable-size fields use offsets - # Calculate base offset (after all fixed fields and offset values) - base_offset = 20 + (5 * 4) # address + 5 offset fields - - # Encode storage_changes - storage_changes_data = ssz_encode_list( - account.storage_changes, - ssz_encode_slot_changes, - MAX_SLOTS - ) - offsets.append(base_offset + len(data_section)) - data_section.extend(storage_changes_data) - - # Encode storage_reads - storage_reads_data = ssz_encode_list( - account.storage_reads, - ssz_encode_slot_read, - MAX_SLOTS - ) - offsets.append(base_offset + len(data_section)) - data_section.extend(storage_reads_data) - - # Encode balance_changes - balance_changes_data = ssz_encode_list( - account.balance_changes, - ssz_encode_balance_change, - MAX_TRANSACTIONS - ) - offsets.append(base_offset + len(data_section)) - data_section.extend(balance_changes_data) - - # Encode nonce_changes - nonce_changes_data = ssz_encode_list( - account.nonce_changes, - ssz_encode_nonce_change, - MAX_TRANSACTIONS - ) - offsets.append(base_offset + len(data_section)) - data_section.extend(nonce_changes_data) - - # Encode code_changes - code_changes_data = ssz_encode_list( - account.code_changes, - ssz_encode_code_change, - MAX_TRANSACTIONS - ) - offsets.append(base_offset + len(data_section)) - data_section.extend(code_changes_data) - - # Write offsets - for offset in offsets: - result.extend(ssz_encode_uint(offset, 4)) - - # Write data section - result.extend(data_section) - - return bytes(result) - - -def ssz_encode_block_access_list(block_access_list: BlockAccessList) -> Bytes: - """ - Encode a [`BlockAccessList`] to SSZ bytes. - - This is the top-level encoding function that produces the final SSZ - representation of a block's access list, following the [SSZ specification] - for Ethereum. - - Parameters - ---------- - block_access_list : - The block access list to encode. - - Returns - ------- - encoded : - The complete SSZ-encoded block access list. - - [`BlockAccessList`]: ref:ethereum.osaka.ssz_types.BlockAccessList - [SSZ specification]: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md - """ - encoded = ssz_encode_list( - block_access_list.account_changes, - ssz_encode_account_changes, - MAX_ACCOUNTS - ) - return Bytes(encoded) - - -def validate_block_access_list_against_execution( - block_access_list: BlockAccessList, - block_access_list_builder: Optional['BlockAccessListBuilder'] = None -) -> bool: - """ - Validate that a Block Access List is structurally correct and optionally matches a builder's state. - - Parameters - ---------- - block_access_list : - The Block Access List to validate. - block_access_list_builder : - Optional Block Access List builder to validate against. If provided, checks that the - Block Access List hash matches what would be built from the builder's current state. - - Returns - ------- - valid : - True if the Block Access List is structurally valid and matches the builder (if provided). - """ - # 1. Validate structural constraints - - # Check that storage changes and reads don't overlap for the same slot - for account in block_access_list.account_changes: - changed_slots = {sc.slot for sc in account.storage_changes} - read_slots = {sr.slot for sr in account.storage_reads} - - # A slot should not be in both changes and reads (per EIP-7928) - if changed_slots & read_slots: - return False - - # 2. Validate ordering (addresses should be sorted lexicographically) - addresses = [account.address for account in block_access_list.account_changes] - if addresses != sorted(addresses): - return False - - # 3. Validate all data is within bounds - max_tx_index = MAX_TRANSACTIONS - 1 - for account in block_access_list.account_changes: - # Validate storage slots are sorted within each account - storage_slots = [sc.slot for sc in account.storage_changes] - if storage_slots != sorted(storage_slots): - return False - - # Check storage changes - for slot_changes in account.storage_changes: - # Check changes are sorted by tx_index - tx_indices = [c.tx_index for c in slot_changes.changes] - if tx_indices != sorted(tx_indices): - return False - - for change in slot_changes.changes: - if change.tx_index > max_tx_index: - return False - - # Check balance changes are sorted by tx_index - balance_tx_indices = [bc.tx_index for bc in account.balance_changes] - if balance_tx_indices != sorted(balance_tx_indices): - return False - - for balance_change in account.balance_changes: - if balance_change.tx_index > max_tx_index: - return False - - # Check nonce changes are sorted by tx_index - nonce_tx_indices = [nc.tx_index for nc in account.nonce_changes] - if nonce_tx_indices != sorted(nonce_tx_indices): - return False - - for nonce_change in account.nonce_changes: - if nonce_change.tx_index > max_tx_index: - return False - - # Check code changes are sorted by tx_index - code_tx_indices = [cc.tx_index for cc in account.code_changes] - if code_tx_indices != sorted(code_tx_indices): - return False - - for code_change in account.code_changes: - if code_change.tx_index > max_tx_index: - return False - if len(code_change.new_code) > MAX_CODE_SIZE: - return False - - # 4. If Block Access List builder provided, validate against it by comparing hashes - if block_access_list_builder is not None: - from .builder import build - # Build a Block Access List from the builder - expected_block_access_list = build(block_access_list_builder) - - # Compare hashes - much simpler! - if compute_block_access_list_hash(block_access_list) != compute_block_access_list_hash(expected_block_access_list): - return False - - return True \ No newline at end of file diff --git a/src/ethereum/forks/amsterdam/blocks.py b/src/ethereum/forks/amsterdam/blocks.py index ef9ddd8777..1177e4c32d 100644 --- a/src/ethereum/forks/amsterdam/blocks.py +++ b/src/ethereum/forks/amsterdam/blocks.py @@ -19,7 +19,6 @@ from ethereum.crypto.hash import Hash32 from .fork_types import Address, Bloom, Root -from .rlp_types import BlockAccessList from .transactions import ( AccessListTransaction, BlobTransaction, @@ -242,14 +241,6 @@ class Header: [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 """ - bal_hash: Hash32 - """ - Hash of the Block Access List containing all accounts and storage - locations accessed during block execution. Introduced in [EIP-7928]. - - [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 - """ - @slotted_freezable @dataclass @@ -303,14 +294,6 @@ class Block: A tuple of withdrawals processed in this block. """ - block_access_list: BlockAccessList - """ - Block Access List containing all accounts and storage locations accessed - during block execution. Introduced in [EIP-7928]. - - [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 - """ - @slotted_freezable @dataclass diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index e538e18cfd..03d2955f2a 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -30,7 +30,6 @@ ) from . import vm -from .block_access_lists import StateChangeTracker, compute_block_access_list_hash, build, set_transaction_index, track_balance_change from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt from .bloom import logs_bloom from .exceptions import ( @@ -55,6 +54,7 @@ from .state import ( State, TransientStorage, + account_exists_and_is_empty, destroy_account, get_account, increment_nonce, @@ -244,10 +244,6 @@ def state_transition(chain: BlockChain, block: Block) -> None: withdrawals_root = root(block_output.withdrawals_trie) requests_hash = compute_requests_hash(block_output.requests) - # Build and validate Block Access List - computed_block_access_list = build(block_output.block_access_list_builder) - computed_block_access_list_hash = compute_block_access_list_hash(computed_block_access_list) - if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( f"{block_output.block_gas_used} != {block.header.gas_used}" @@ -266,10 +262,6 @@ def state_transition(chain: BlockChain, block: Block) -> None: raise InvalidBlock if requests_hash != block.header.requests_hash: raise InvalidBlock - if computed_bal_hash != block.header.bal_hash: - raise InvalidBlock - if computed_block_access_list != block.block_access_list: - raise InvalidBlock chain.blocks.append(block) if len(chain.blocks) > 255: @@ -589,7 +581,6 @@ def process_system_transaction( target_address: Address, system_contract_code: Bytes, data: Bytes, - change_tracker: Optional[StateChangeTracker] = None, ) -> MessageCallOutput: """ Process a system transaction with the given code. @@ -645,7 +636,6 @@ def process_system_transaction( accessed_storage_keys=set(), disable_precompiles=False, parent_evm=None, - change_tracker=change_tracker, ) system_tx_output = process_message_call(system_tx_message) @@ -657,7 +647,6 @@ def process_checked_system_transaction( block_env: vm.BlockEnvironment, target_address: Address, data: Bytes, - change_tracker: Optional[StateChangeTracker] = None, ) -> MessageCallOutput: """ Process a system transaction and raise an error if the contract does not @@ -690,7 +679,6 @@ def process_checked_system_transaction( target_address, system_contract_code, data, - change_tracker, ) if system_tx_output.error: @@ -706,7 +694,6 @@ def process_unchecked_system_transaction( block_env: vm.BlockEnvironment, target_address: Address, data: Bytes, - change_tracker: Optional[StateChangeTracker] = None, ) -> MessageCallOutput: """ Process a system transaction without checking if the contract contains code @@ -732,7 +719,6 @@ def process_unchecked_system_transaction( target_address, system_contract_code, data, - change_tracker, ) @@ -767,42 +753,26 @@ def apply_body( """ block_output = vm.BlockOutput() - # Initialize Block Access List state change tracker - change_tracker = StateChangeTracker(block_output.block_access_list_builder) - - # Set system transaction index for pre-execution system contracts - # EIP-7928: System contracts use bal_index 0 - set_transaction_index(change_tracker, 0) - process_unchecked_system_transaction( block_env=block_env, target_address=BEACON_ROOTS_ADDRESS, data=block_env.parent_beacon_block_root, - change_tracker=change_tracker, ) process_unchecked_system_transaction( block_env=block_env, target_address=HISTORY_STORAGE_ADDRESS, data=block_env.block_hashes[-1], # The parent hash - change_tracker=change_tracker, ) - # EIP-7928: Transactions use bal_index 1 to len(transactions) for i, tx in enumerate(map(decode_transaction, transactions)): - set_transaction_index(change_tracker, i + 1) - process_transaction(block_env, block_output, tx, Uint(i), change_tracker) + process_transaction(block_env, block_output, tx, Uint(i)) - # EIP-7928: Post-execution uses bal_index len(transactions) + 1 - post_execution_index = len(transactions) + 1 - set_transaction_index(change_tracker, post_execution_index) - - process_withdrawals(block_env, block_output, withdrawals, change_tracker) + process_withdrawals(block_env, block_output, withdrawals) process_general_purpose_requests( block_env=block_env, block_output=block_output, - change_tracker=change_tracker, ) return block_output @@ -811,7 +781,6 @@ def apply_body( def process_general_purpose_requests( block_env: vm.BlockEnvironment, block_output: vm.BlockOutput, - change_tracker: StateChangeTracker, ) -> None: """ Process all the requests in the block. @@ -833,7 +802,6 @@ def process_general_purpose_requests( block_env=block_env, target_address=WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, data=b"", - change_tracker=change_tracker, ) if len(system_withdrawal_tx_output.return_data) > 0: @@ -845,7 +813,6 @@ def process_general_purpose_requests( block_env=block_env, target_address=CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, data=b"", - change_tracker=change_tracker, ) if len(system_consolidation_tx_output.return_data) > 0: @@ -860,14 +827,13 @@ def process_transaction( block_output: vm.BlockOutput, tx: Transaction, index: Uint, - change_tracker: StateChangeTracker, ) -> 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, + It decrements the sender's account 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. @@ -914,13 +880,13 @@ def process_transaction( effective_gas_fee = tx.gas * effective_gas_price gas = tx.gas - intrinsic_gas - increment_nonce(block_env.state, sender, change_tracker) + increment_nonce(block_env.state, sender) sender_balance_after_gas_fee = ( Uint(sender_account.balance) - effective_gas_fee - blob_gas_fee ) set_account_balance( - block_env.state, sender, U256(sender_balance_after_gas_fee), change_tracker + block_env.state, sender, U256(sender_balance_after_gas_fee) ) access_list_addresses = set() @@ -958,7 +924,6 @@ def process_transaction( ) message = prepare_message(block_env, tx_env, tx) - message.change_tracker = change_tracker tx_output = process_message_call(message) @@ -987,18 +952,20 @@ def process_transaction( sender_balance_after_refund = get_account( block_env.state, sender ).balance + U256(gas_refund_amount) - set_account_balance(block_env.state, sender, sender_balance_after_refund, change_tracker) + set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees coinbase_balance_after_mining_fee = get_account( block_env.state, block_env.coinbase ).balance + U256(transaction_fee) - set_account_balance( - block_env.state, - block_env.coinbase, - coinbase_balance_after_mining_fee, - change_tracker - ) + if coinbase_balance_after_mining_fee != 0: + set_account_balance( + block_env.state, + block_env.coinbase, + coinbase_balance_after_mining_fee, + ) + elif account_exists_and_is_empty(block_env.state, block_env.coinbase): + destroy_account(block_env.state, block_env.coinbase) for address in tx_output.accounts_to_delete: destroy_account(block_env.state, address) @@ -1026,7 +993,6 @@ def process_withdrawals( block_env: vm.BlockEnvironment, block_output: vm.BlockOutput, withdrawals: Tuple[Withdrawal, ...], - change_tracker: StateChangeTracker, ) -> None: """ Increase the balance of the withdrawing account. @@ -1044,9 +1010,8 @@ def increase_recipient_balance(recipient: Account) -> None: modify_state(block_env.state, wd.address, increase_recipient_balance) - # Track balance change for BAL (withdrawals are tracked as system contract changes) - new_balance = get_account(block_env.state, wd.address).balance - track_balance_change(change_tracker, wd.address, U256(new_balance), block_env.state) + if account_exists_and_is_empty(block_env.state, wd.address): + destroy_account(block_env.state, wd.address) def check_gas_limit(gas_limit: Uint, parent_gas_limit: Uint) -> bool: diff --git a/src/ethereum/forks/amsterdam/ssz_types.py b/src/ethereum/forks/amsterdam/ssz_types.py deleted file mode 100644 index 4e68ac1818..0000000000 --- a/src/ethereum/forks/amsterdam/ssz_types.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -SSZ Types for EIP-7928 Block-Level Access Lists -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This module defines the SSZ data structures for Block-Level Access Lists -as specified in EIP-7928. These structures enable efficient encoding and -decoding of all accounts and storage locations accessed during block execution. -""" - -from dataclasses import dataclass -from typing import List, Tuple - -from ethereum_types.bytes import Bytes, Bytes20, Bytes32 -from ethereum_types.frozen import slotted_freezable -from ethereum_types.numeric import U256, Uint - -# Type aliases for clarity -Address = Bytes20 -StorageKey = Bytes32 -StorageValue = Bytes32 -TxIndex = Uint -Balance = Bytes # uint128 - Post-transaction balance in wei (16 bytes, sufficient for total ETH supply) -Nonce = Uint - -# Constants chosen to support a 630m block gas limit -MAX_TRANSACTIONS = 30_000 -MAX_SLOTS = 300_000 -MAX_ACCOUNTS = 300_000 -MAX_CODE_SIZE = 24_576 -MAX_CODE_CHANGES = 1 - - -@slotted_freezable -@dataclass -class StorageChange: - """Single storage write: tx_index -> new_value""" - tx_index: TxIndex - new_value: StorageValue - - -@slotted_freezable -@dataclass -class BalanceChange: - """Single balance change: tx_index -> post_balance""" - tx_index: TxIndex - post_balance: Balance - - -@slotted_freezable -@dataclass -class NonceChange: - """Single nonce change: tx_index -> new_nonce""" - tx_index: TxIndex - new_nonce: Nonce - - -@slotted_freezable -@dataclass -class CodeChange: - """Single code change: tx_index -> new_code""" - tx_index: TxIndex - new_code: Bytes - - -@slotted_freezable -@dataclass -class SlotChanges: - """All changes to a single storage slot""" - slot: StorageKey - changes: Tuple[StorageChange, ...] - - - - -@slotted_freezable -@dataclass -class AccountChanges: - """ - All changes for a single account, grouped by field type. - This eliminates address redundancy across different change types. - """ - address: Address - storage_changes: Tuple[SlotChanges, ...] - storage_reads: Tuple[StorageKey, ...] - balance_changes: Tuple[BalanceChange, ...] - nonce_changes: Tuple[NonceChange, ...] - code_changes: Tuple[CodeChange, ...] - - -@slotted_freezable -@dataclass -class BlockAccessList: - """ - Block-Level Access List for EIP-7928. - Contains all addresses accessed during block execution. - """ - account_changes: Tuple[AccountChanges, ...] \ No newline at end of file diff --git a/src/ethereum/forks/amsterdam/state.py b/src/ethereum/forks/amsterdam/state.py index 28feb53eb5..7cec40084d 100644 --- a/src/ethereum/forks/amsterdam/state.py +++ b/src/ethereum/forks/amsterdam/state.py @@ -27,11 +27,6 @@ from .fork_types import EMPTY_ACCOUNT, Account, Address, Root from .trie import EMPTY_TRIE_ROOT, Trie, copy_trie, root, trie_get, trie_set -# Forward declaration for type hints -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from .block_access_lists import StateChangeTracker - @dataclass class State: @@ -476,7 +471,6 @@ def move_ether( sender_address: Address, recipient_address: Address, amount: U256, - change_tracker: "StateChangeTracker", ) -> None: """ Move funds between accounts. @@ -492,22 +486,9 @@ def increase_recipient_balance(recipient: Account) -> None: modify_state(state, sender_address, reduce_sender_balance) modify_state(state, recipient_address, increase_recipient_balance) - - if change_tracker is not None: - from .block_access_lists.tracker import track_balance_change - sender_new_balance = get_account(state, sender_address).balance - recipient_new_balance = get_account(state, recipient_address).balance - - track_balance_change(change_tracker, sender_address, U256(sender_new_balance), state) - track_balance_change(change_tracker, recipient_address, U256(recipient_new_balance), state) - - -def set_account_balance( - state: State, - address: Address, - amount: U256, - change_tracker: "StateChangeTracker", -) -> None: + + +def set_account_balance(state: State, address: Address, amount: U256) -> None: """ Sets the balance of an account. @@ -521,22 +502,15 @@ def set_account_balance( amount: The amount that needs to set in balance. - - change_tracker: - Change tracker to record balance changes. """ def set_balance(account: Account) -> None: account.balance = amount modify_state(state, address, set_balance) - - if change_tracker is not None: - from .block_access_lists.tracker import track_balance_change - track_balance_change(change_tracker, address, amount, state) -def increment_nonce(state: State, address: Address, change_tracker: "StateChangeTracker") -> None: +def increment_nonce(state: State, address: Address) -> None: """ Increments the nonce of an account. @@ -547,33 +521,15 @@ def increment_nonce(state: State, address: Address, change_tracker: "StateChange address: Address of the account whose nonce needs to be incremented. - - change_tracker: - Change tracker for EIP-7928. """ def increase_nonce(sender: Account) -> None: sender.nonce += Uint(1) modify_state(state, address, increase_nonce) - - # Track nonce change for Block Access List (for ALL accounts and ALL nonce changes) - # This includes: - # - EOA senders (transaction nonce increments) - # - Contracts performing CREATE/CREATE2 - # - Deployed contracts - # - EIP-7702 authorities - from .block_access_lists.tracker import track_nonce_change - account = get_account(state, address) - track_nonce_change(change_tracker, address, account.nonce, state) -def set_code( - state: State, - address: Address, - code: Bytes, - change_tracker: "StateChangeTracker", -) -> None: +def set_code(state: State, address: Address, code: Bytes) -> None: """ Sets Account code. @@ -587,18 +543,12 @@ def set_code( code: The bytecode that needs to be set. - - change_tracker: - Change tracker for EIP-7928. """ def write_code(sender: Account) -> None: sender.code = code modify_state(state, address, write_code) - - from .block_access_lists.tracker import track_code_change - track_code_change(change_tracker, address, code, state) def get_storage_original(state: State, address: Address, key: Bytes32) -> U256: diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index f3de485fdf..033293a5fd 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -22,17 +22,12 @@ from ethereum.crypto.hash import Hash32 from ethereum.exceptions import EthereumException -from ..block_access_lists import BlockAccessListBuilder from ..blocks import Log, Receipt, Withdrawal from ..fork_types import Address, Authorization, VersionedHash from ..state import State, TransientStorage from ..transactions import LegacyTransaction from ..trie import Trie -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from ..block_access_lists import StateChangeTracker - __all__ = ("Environment", "Evm", "Message") @@ -95,7 +90,6 @@ class BlockOutput: ) blob_gas_used: U64 = U64(0) requests: List[Bytes] = field(default_factory=list) - block_access_list_builder: BlockAccessListBuilder = field(default_factory=BlockAccessListBuilder) @dataclass @@ -139,7 +133,6 @@ class Message: accessed_storage_keys: Set[Tuple[Address, Bytes32]] disable_precompiles: bool parent_evm: Optional["Evm"] - change_tracker: Optional["StateChangeTracker"] = None @dataclass diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py index ecf64b524f..1fe2e1e7bd 100644 --- a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -195,9 +195,9 @@ def set_delegation(message: Message) -> U256: code_to_set = b"" else: code_to_set = EOA_DELEGATION_MARKER + auth.address - set_code(state, authority, code_to_set, message.change_tracker) + set_code(state, authority, code_to_set) - increment_nonce(state, authority, message.change_tracker) + increment_nonce(state, authority) if message.code_address is None: raise InvalidBlock("Invalid type 4 transaction: no target") diff --git a/src/ethereum/forks/amsterdam/vm/instructions/environment.py b/src/ethereum/forks/amsterdam/vm/instructions/environment.py index 6d144a0087..226b3d3bb3 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/environment.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/environment.py @@ -86,10 +86,6 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. balance = get_account(evm.message.block_env.state, address).balance - - if evm.message.change_tracker: - from ...block_access_lists.tracker import track_address_access - track_address_access(evm.message.change_tracker, address) push(evm.stack, balance) @@ -356,10 +352,6 @@ def extcodesize(evm: Evm) -> None: # OPERATION code = get_account(evm.message.block_env.state, address).code - - if evm.message.change_tracker: - from ...block_access_lists.tracker import track_address_access - track_address_access(evm.message.change_tracker, address) codesize = U256(len(code)) push(evm.stack, codesize) @@ -402,10 +394,6 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by code = get_account(evm.message.block_env.state, address).code - - if evm.message.change_tracker: - from ...block_access_lists.tracker import track_address_access - track_address_access(evm.message.change_tracker, address) value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -492,10 +480,6 @@ def extcodehash(evm: Evm) -> None: # OPERATION account = get_account(evm.message.block_env.state, address) - - if evm.message.change_tracker: - from ...block_access_lists.tracker import track_address_access - track_address_access(evm.message.change_tracker, address) if account == EMPTY_ACCOUNT: codehash = U256(0) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py index 38ba054356..65a0d5a9b6 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -59,15 +59,6 @@ def sload(evm: Evm) -> None: value = get_storage( evm.message.block_env.state, evm.message.current_target, key ) - - if evm.message.change_tracker: - from ...block_access_lists.tracker import track_storage_read - track_storage_read( - evm.message.change_tracker, - evm.message.current_target, - key, - evm.message.block_env.state - ) push(evm.stack, value) @@ -136,13 +127,6 @@ def sstore(evm: Evm) -> None: if evm.message.is_static: raise WriteInStaticContext set_storage(state, evm.message.current_target, key, new_value) - - if evm.message.change_tracker: - from ...block_access_lists.tracker import track_storage_write - track_storage_write( - evm.message.change_tracker, - evm.message.current_target, key, new_value, state - ) # PROGRAM COUNTER evm.pc += Uint(1) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index 562c0b59be..d7308821bd 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -108,12 +108,12 @@ def generic_create( evm.message.block_env.state, contract_address ) or account_has_storage(evm.message.block_env.state, contract_address): increment_nonce( - evm.message.block_env.state, evm.message.current_target, evm.message.change_tracker + evm.message.block_env.state, evm.message.current_target ) push(evm.stack, U256(0)) return - increment_nonce(evm.message.block_env.state, evm.message.current_target, evm.message.change_tracker) + increment_nonce(evm.message.block_env.state, evm.message.current_target) child_message = Message( block_env=evm.message.block_env, @@ -133,13 +133,7 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=False, parent_evm=evm, - change_tracker=evm.message.change_tracker, ) - - if evm.message.change_tracker: - from ...block_access_lists.tracker import track_address_access - track_address_access(evm.message.change_tracker, contract_address) - child_evm = process_create_message(child_message) if child_evm.error: @@ -329,13 +323,7 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=disable_precompiles, parent_evm=evm, - change_tracker=evm.message.change_tracker, ) - - if evm.message.change_tracker: - from ...block_access_lists.tracker import track_address_access - track_address_access(evm.message.change_tracker, to) - child_evm = process_message(child_message) if child_evm.error: @@ -566,7 +554,6 @@ def selfdestruct(evm: Evm) -> None: originator, beneficiary, originator_balance, - evm.message.change_tracker ) # register account for deletion only if it was created @@ -574,7 +561,7 @@ def selfdestruct(evm: Evm) -> None: if originator in evm.message.block_env.state.created_accounts: # If beneficiary is the same as originator, then # the ether is burnt. - set_account_balance(evm.message.block_env.state, originator, U256(0), evm.message.change_tracker) + set_account_balance(evm.message.block_env.state, originator, U256(0)) evm.accounts_to_delete.add(originator) # HALT the execution diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index 26afdfefb6..fb893aaa6b 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -192,7 +192,7 @@ def process_create_message(message: Message) -> Evm: # added to SELFDESTRUCT by EIP-6780. mark_account_created(state, message.current_target) - increment_nonce(state, message.current_target, message.change_tracker) + increment_nonce(state, message.current_target) evm = process_message(message) if not evm.error: contract_code = evm.output @@ -210,7 +210,7 @@ def process_create_message(message: Message) -> Evm: evm.output = b"" evm.error = error else: - set_code(state, message.current_target, contract_code, message.change_tracker) + set_code(state, message.current_target, contract_code) commit_transaction(state, transient_storage) else: rollback_transaction(state, transient_storage) @@ -241,8 +241,7 @@ def process_message(message: Message) -> Evm: if message.should_transfer_value and message.value != 0: move_ether( - state, message.caller, message.current_target, message.value, - message.change_tracker + state, message.caller, message.current_target, message.value ) evm = execute_code(message) diff --git a/src/ethereum/genesis.py b/src/ethereum/genesis.py index 0f836e5f12..e11e9962b5 100644 --- a/src/ethereum/genesis.py +++ b/src/ethereum/genesis.py @@ -233,6 +233,7 @@ def add_genesis_block( "timestamp": genesis.timestamp, "extra_data": genesis.extra_data, "nonce": genesis.nonce, + } if has_field(hardfork.Header, "mix_digest"): @@ -258,6 +259,9 @@ def add_genesis_block( if has_field(hardfork.Header, "requests_hash"): fields["requests_hash"] = Hash32(b"\0" * 32) + if has_field(hardfork.Header, "bal_hash"): + fields["bal_hash"] = Hash32(b"\0" * 32) + genesis_header = hardfork.Header(**fields) block_fields = { @@ -272,6 +276,9 @@ def add_genesis_block( if has_field(hardfork.Block, "requests"): block_fields["requests"] = () + if has_field(hardfork.Block, "block_access_list"): + block_fields["block_access_list"] = rlp.encode([]) + genesis_block = hardfork.Block(**block_fields) chain.blocks.append(genesis_block) diff --git a/src/ethereum_spec_tools/evm_tools/daemon.py b/src/ethereum_spec_tools/evm_tools/daemon.py index 6a617616f6..ebb8b8c650 100644 --- a/src/ethereum_spec_tools/evm_tools/daemon.py +++ b/src/ethereum_spec_tools/evm_tools/daemon.py @@ -109,9 +109,7 @@ def do_POST(self) -> None: # noqa N802 # `self.wfile` is missing the `name` attribute so it doesn't strictly # satisfy the bounds for `TextIOWrapper`. Fortunately nothing uses # `name` so far, so we can safely ignore the error. - with TextIOWrapper( - self.wfile, encoding="utf-8" # type: ignore[type-var] - ) as out_wrapper: + with TextIOWrapper(self.wfile, encoding="utf-8") as out_wrapper: # type: ignore # noqa: E501 main(args=args, out_file=out_wrapper, in_file=input) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index 92f3d52b0c..96d0c89519 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -12,6 +12,13 @@ from ethereum_types.numeric import U64, U256, Uint from ethereum import trace +from ethereum.forks.amsterdam.block_access_lists import ( + StateChangeTracker, + set_transaction_index, +) +from ethereum.amsterdam.block_access_lists.tracker import ( + finalize_transaction_changes, +) from ethereum.exceptions import EthereumException, InvalidBlock from ethereum_spec_tools.forks import Hardfork @@ -237,12 +244,24 @@ def run_state_test(self) -> Any: if len(self.txs.transactions) > 0: tx = self.txs.transactions[0] try: - self.fork.process_transaction( - block_env=block_env, - block_output=block_output, - tx=tx, - index=Uint(0), - ) + # Only pass change_tracker for Amsterdam and later + if self.fork.is_after_fork("ethereum.forks.amsterdam"): + self.fork.process_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + index=Uint(0), + change_tracker=StateChangeTracker( + block_output.block_access_list_builder + ), + ) + else: + self.fork.process_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + index=Uint(0), + ) except EthereumException as e: self.txs.rejected_txs[0] = f"Failed transaction: {e!r}" self.restore_state() @@ -252,32 +271,69 @@ def run_state_test(self) -> Any: self.result.rejected = self.txs.rejected_txs def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: - if self.fork.is_after_fork("ethereum.forks.prague"): - self.fork.process_unchecked_system_transaction( - block_env=block_env, - target_address=self.fork.HISTORY_STORAGE_ADDRESS, - data=block_env.block_hashes[-1], # The parent hash + bal_change_tracker = None + if self.fork.is_after_fork("ethereum.forks.amsterdam"): + bal_change_tracker = StateChangeTracker( + block_output.block_access_list_builder ) + # EIP-7928: Set transaction index for block access lists + # pre-execution system contracts use index 0 + set_transaction_index(bal_change_tracker, 0) - if self.fork.is_after_fork("ethereum.forks.cancun"): - self.fork.process_unchecked_system_transaction( - block_env=block_env, - target_address=self.fork.BEACON_ROOTS_ADDRESS, - data=block_env.parent_beacon_block_root, - ) + if self.fork.is_after_fork("ethereum.forks.prague"): + process_args = { + "block_env": block_env, + "target_address": self.fork.HISTORY_STORAGE_ADDRESS, + "data": block_env.block_hashes[-1], # The parent hash + } + if self.fork.is_after_fork("ethereum.forks.amsterdam"): + process_args["change_tracker"] = bal_change_tracker + self.fork.process_unchecked_system_transaction(**process_args) - for i, tx in zip( + if self.fork.is_after_fork("ethereum.forks.cancun"): + process_args = { + "block_env": block_env, + "target_address": self.fork.BEACON_ROOTS_ADDRESS, + "data": block_env.parent_beacon_block_root, + } + if self.fork.is_after_fork("ethereum.forks.amsterdam"): + process_args["change_tracker"] = bal_change_tracker + self.fork.process_unchecked_system_transaction(**process_args) + + for tx_index, (original_idx, tx) in enumerate(zip( self.txs.successfully_parsed, self.txs.transactions, strict=True, - ): + )): self.backup_state() try: - self.fork.process_transaction( - block_env, block_output, tx, Uint(i) - ) + process_tx_args = [ + block_env, + block_output, + tx, + Uint(original_idx), + ] + + if self.fork.is_after_fork("ethereum.forks.amsterdam"): + process_tx_args.append(bal_change_tracker) + + assert bal_change_tracker is not None + # use 1...n for transaction indices + set_transaction_index(bal_change_tracker, tx_index + 1) + self.fork.process_transaction(*process_tx_args) + finalize_transaction_changes( + bal_change_tracker, + block_env.state, + ) + else: + self.fork.process_transaction(*process_tx_args) + except EthereumException as e: - self.txs.rejected_txs[i] = f"Failed transaction: {e!r}" + self.txs.rejected_txs[original_idx] = ( + f"Failed transaction: {e!r}" + ) self.restore_state() - self.logger.warning(f"Transaction {i} failed: {e!r}") + self.logger.warning( + f"Transaction {original_idx} failed: {e!r}" + ) if not self.fork.is_after_fork("ethereum.forks.paris"): if self.options.state_reward is None: @@ -288,12 +344,34 @@ def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: ) if self.fork.is_after_fork("ethereum.forks.shanghai"): - self.fork.process_withdrawals( + process_withdrawal_args = [ block_env, block_output, self.env.withdrawals - ) + ] + + if self.fork.is_after_fork("ethereum.forks.amsterdam"): + assert bal_change_tracker is not None + process_withdrawal_args.append(bal_change_tracker) + + self.fork.process_withdrawals(*process_withdrawal_args) if self.fork.is_after_fork("ethereum.forks.prague"): - self.fork.process_general_purpose_requests(block_env, block_output) + process_general_purpose_args = [block_env, block_output] + + if self.fork.is_after_fork("ethereum.forks.amsterdam"): + assert bal_change_tracker is not None + process_general_purpose_args.append(bal_change_tracker) + + self.fork.process_general_purpose_requests(*process_general_purpose_args) + + if self.fork.is_after_fork("ethereum.forks.amsterdam"): + assert bal_change_tracker is not None + num_transactions = len( + [tx for tx in self.txs.successfully_parsed if tx] + ) + + # post-execution use n + 1 + post_execution_index = num_transactions + 1 + set_transaction_index(bal_change_tracker, post_execution_index) def run_blockchain_test(self) -> None: """ 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 4d4d6a0e0b..bfa67c7a4d 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -268,6 +268,8 @@ class Result: requests_hash: Optional[Hash32] = None requests: Optional[List[Bytes]] = None block_exception: Optional[str] = None + block_access_list: Optional[Any] = None + block_access_list_hash: Optional[Hash32] = None def get_receipts_from_output( self, @@ -323,6 +325,91 @@ def update(self, t8n: "T8N", block_env: Any, block_output: Any) -> None: self.requests = block_output.requests self.requests_hash = t8n.fork.compute_requests_hash(self.requests) + if hasattr(block_output, "block_access_list_builder"): + from ethereum.amsterdam.block_access_lists import ( + build, + compute_block_access_list_hash, + ) + + bal = build(block_output.block_access_list_builder) + self.block_access_list = ( + bal # Store the BAL object directly, not RLP + ) + self.block_access_list_hash = compute_block_access_list_hash(bal) + + def _bal_to_json(self, bal: Any) -> Any: + """ + Convert BlockAccessList to JSON format matching the Pydantic models. + """ + account_changes = [] + + for account in bal.account_changes: + account_data: Dict[str, Any] = { + "address": "0x" + account.address.hex() + } + + # Add storage changes if present + if account.storage_changes: + storage_changes = [] + for slot_change in account.storage_changes: + slot_data: Dict[str, Any] = { + "slot": int.from_bytes(slot_change.slot, "big"), + "slotChanges": [], + } + for change in slot_change.changes: + slot_data["slotChanges"].append( + { + "txIndex": int(change.block_access_index), + "postValue": int.from_bytes( + change.new_value, "big" + ), + } + ) + storage_changes.append(slot_data) + account_data["storageChanges"] = storage_changes + + # Add storage reads if present + if account.storage_reads: + account_data["storageReads"] = [ + int.from_bytes(slot, "big") + for slot in account.storage_reads + ] + + # Add balance changes if present + if account.balance_changes: + account_data["balanceChanges"] = [ + { + "txIndex": int(change.block_access_index), + "postBalance": int(change.post_balance), + } + for change in account.balance_changes + ] + + # Add nonce changes if present + if account.nonce_changes: + account_data["nonceChanges"] = [ + { + "txIndex": int(change.block_access_index), + "postNonce": int(change.new_nonce), + } + for change in account.nonce_changes + ] + + # Add code changes if present + if account.code_changes: + account_data["codeChanges"] = [ + { + "txIndex": int(change.block_access_index), + "newCode": "0x" + change.new_code.hex(), + } + for change in account.code_changes + ] + + account_changes.append(account_data) + + # return as list directly + return account_changes + def json_encode_receipts(self) -> Any: """ Encode receipts to JSON. @@ -390,4 +477,13 @@ def to_json(self) -> Any: if self.block_exception is not None: data["blockException"] = self.block_exception + if self.block_access_list is not None: + # Convert BAL to JSON format + data["blockAccessList"] = self._bal_to_json(self.block_access_list) + + if self.block_access_list_hash is not None: + data["blockAccessListHash"] = encode_to_hex( + self.block_access_list_hash + ) + return data diff --git a/tests/amsterdam/conftest.py b/tests/amsterdam/conftest.py new file mode 100644 index 0000000000..bd5219f247 --- /dev/null +++ b/tests/amsterdam/conftest.py @@ -0,0 +1,12 @@ +""" +Minimal conftest for amsterdam BAL tests. +""" +from typing import Any + + +def pytest_configure(config: Any) -> None: + """Configure custom markers.""" + config.addinivalue_line("markers", "bal: mark test as BAL-related") + config.addinivalue_line( + "markers", "integration: mark test as integration test" + ) diff --git a/tests/osaka/test_bal_implementation.py b/tests/amsterdam/test_bal_implementation.py similarity index 66% rename from tests/osaka/test_bal_implementation.py rename to tests/amsterdam/test_bal_implementation.py index fdbd56d205..8d0db99a4a 100644 --- a/tests/osaka/test_bal_implementation.py +++ b/tests/amsterdam/test_bal_implementation.py @@ -8,169 +8,174 @@ - Edge cases and error handling """ -import ast -from pathlib import Path from unittest.mock import MagicMock, patch import pytest - from ethereum_types.bytes import Bytes, Bytes20, Bytes32 -from ethereum_types.numeric import U32, U64, U256, Uint +from ethereum_types.numeric import U64, U256, Uint -from ethereum.osaka.block_access_lists import ( +from ethereum.amsterdam.block_access_lists import ( BlockAccessListBuilder, StateChangeTracker, - add_storage_write, - add_storage_read, add_balance_change, - add_nonce_change, add_code_change, + add_nonce_change, + add_storage_read, + add_storage_write, add_touched_account, build, ) -from ethereum.osaka.rlp_types import ( - AccountChanges, - BalanceChange, - BlockAccessList, - BlockAccessIndex, - CodeChange, - NonceChange, - SlotChanges, - StorageChange, +from ethereum.amsterdam.block_access_lists.tracker import ( + capture_pre_state, + set_transaction_index, + track_balance_change, + track_code_change, + track_nonce_change, + track_storage_write, +) +from ethereum.amsterdam.rlp_types import ( MAX_CODE_CHANGES, + BlockAccessIndex, + BlockAccessList, ) class TestBALCore: """Test core BAL functionality.""" - - def test_bal_builder_initialization(self): + + def test_bal_builder_initialization(self) -> None: """Test BAL builder initializes correctly.""" builder = BlockAccessListBuilder() assert builder.accounts == {} - - def test_bal_builder_add_storage_write(self): + + def test_bal_builder_add_storage_write(self) -> None: """Test adding storage writes to BAL builder.""" builder = BlockAccessListBuilder() - address = Bytes20(b'\x01' * 20) - slot = Bytes32(b'\x02' * 32) - value = Bytes32(b'\x03' * 32) - + address = Bytes20(b"\x01" * 20) + slot = Bytes32(b"\x02" * 32) + value = Bytes32(b"\x03" * 32) + add_storage_write(builder, address, slot, BlockAccessIndex(0), value) - + assert address in builder.accounts assert slot in builder.accounts[address].storage_changes assert len(builder.accounts[address].storage_changes[slot]) == 1 - + change = builder.accounts[address].storage_changes[slot][0] assert change.block_access_index == 0 assert change.new_value == value - - def test_bal_builder_add_storage_read(self): + + def test_bal_builder_add_storage_read(self) -> None: """Test adding storage reads to BAL builder.""" builder = BlockAccessListBuilder() - address = Bytes20(b'\x01' * 20) - slot = Bytes32(b'\x02' * 32) - + address = Bytes20(b"\x01" * 20) + slot = Bytes32(b"\x02" * 32) + add_storage_read(builder, address, slot) - + assert address in builder.accounts assert slot in builder.accounts[address].storage_reads - - def test_bal_builder_add_balance_change(self): + + def test_bal_builder_add_balance_change(self) -> None: """Test adding balance changes to BAL builder.""" builder = BlockAccessListBuilder() - address = Bytes20(b'\x01' * 20) - balance = Bytes(b'\x00' * 16) # uint128 - + address = Bytes20(b"\x01" * 20) + balance = U256(0) + add_balance_change(builder, address, BlockAccessIndex(0), balance) - + assert address in builder.accounts assert len(builder.accounts[address].balance_changes) == 1 - + change = builder.accounts[address].balance_changes[0] assert change.block_access_index == 0 assert change.post_balance == balance - - def test_bal_builder_add_nonce_change(self): + + def test_bal_builder_add_nonce_change(self) -> None: """Test adding nonce changes to BAL builder.""" builder = BlockAccessListBuilder() - address = Bytes20(b'\x01' * 20) + address = Bytes20(b"\x01" * 20) nonce = 42 - + add_nonce_change(builder, address, BlockAccessIndex(0), U64(nonce)) - + assert address in builder.accounts assert len(builder.accounts[address].nonce_changes) == 1 - + change = builder.accounts[address].nonce_changes[0] assert change.block_access_index == 0 assert change.new_nonce == U64(42) - - def test_bal_builder_add_code_change(self): + + def test_bal_builder_add_code_change(self) -> None: """Test adding code changes to BAL builder.""" builder = BlockAccessListBuilder() - address = Bytes20(b'\x01' * 20) - code = Bytes(b'\x60\x80\x60\x40') - + address = Bytes20(b"\x01" * 20) + code = Bytes(b"\x60\x80\x60\x40") + add_code_change(builder, address, BlockAccessIndex(0), code) - + assert address in builder.accounts assert len(builder.accounts[address].code_changes) == 1 - + change = builder.accounts[address].code_changes[0] assert change.block_access_index == 0 assert change.new_code == code - - def test_bal_builder_touched_account(self): + + def test_bal_builder_touched_account(self) -> None: """Test adding touched accounts without changes.""" builder = BlockAccessListBuilder() - address = Bytes20(b'\x01' * 20) - + address = Bytes20(b"\x01" * 20) + add_touched_account(builder, address) - + assert address in builder.accounts assert builder.accounts[address].storage_changes == {} assert builder.accounts[address].storage_reads == set() assert builder.accounts[address].balance_changes == [] assert builder.accounts[address].nonce_changes == [] assert builder.accounts[address].code_changes == [] - - def test_bal_builder_build_complete(self): + + def test_bal_builder_build_complete(self) -> None: """Test building a complete BlockAccessList.""" builder = BlockAccessListBuilder() - + # Add various changes - address1 = Bytes20(b'\x01' * 20) - address2 = Bytes20(b'\x02' * 20) - slot1 = Bytes32(b'\x03' * 32) - slot2 = Bytes32(b'\x04' * 32) - + address1 = Bytes20(b"\x01" * 20) + address2 = Bytes20(b"\x02" * 20) + slot1 = Bytes32(b"\x03" * 32) + slot2 = Bytes32(b"\x04" * 32) + # Address 1: storage write and read - add_storage_write(builder, address1, slot1, BlockAccessIndex(1), Bytes32(b'\x05' * 32)) + add_storage_write( + builder, + address1, + slot1, + BlockAccessIndex(1), + Bytes32(b"\x05" * 32), + ) add_storage_read(builder, address1, slot2) - add_balance_change(builder, address1, BlockAccessIndex(1), Bytes(b'\x00' * 16)) - + add_balance_change(builder, address1, BlockAccessIndex(1), U256(0)) + # Address 2: only touched add_touched_account(builder, address2) - + # Build BAL block_access_list = build(builder) - + assert isinstance(block_access_list, BlockAccessList) assert len(block_access_list.account_changes) == 2 - + # Verify sorting by address assert block_access_list.account_changes[0].address == address1 assert block_access_list.account_changes[1].address == address2 - + # Verify address1 changes acc1 = block_access_list.account_changes[0] assert len(acc1.storage_changes) == 1 assert len(acc1.storage_reads) == 1 assert acc1.storage_reads[0] == slot2 # Direct StorageKey assert len(acc1.balance_changes) == 1 - + # Verify address2 is empty acc2 = block_access_list.account_changes[1] assert len(acc2.storage_changes) == 0 @@ -180,157 +185,153 @@ def test_bal_builder_build_complete(self): class TestBALTracker: """Test BAL state change tracker functionality.""" - - def test_tracker_initialization(self): + + def test_tracker_initialization(self) -> None: """Test tracker initializes with BAL builder.""" builder = BlockAccessListBuilder() tracker = StateChangeTracker(builder) assert tracker.block_access_list_builder is builder assert tracker.pre_storage_cache == {} assert tracker.current_block_access_index == 0 - - def test_tracker_set_transaction_index(self): + + def test_tracker_set_transaction_index(self) -> None: """Test setting block access index.""" - from ethereum.osaka.block_access_lists import set_transaction_index builder = BlockAccessListBuilder() tracker = StateChangeTracker(builder) - + set_transaction_index(tracker, 5) assert tracker.current_block_access_index == 5 # Pre-storage cache should persist across transactions assert tracker.pre_storage_cache == {} - - @patch('ethereum.osaka.block_access_lists.tracker.get_storage') - def test_tracker_capture_pre_state(self, mock_get_storage): + + @patch("ethereum.amsterdam.state.get_storage") + def test_tracker_capture_pre_state( + self, mock_get_storage: MagicMock + ) -> None: """Test capturing pre-state values.""" - from ethereum.osaka.block_access_lists.tracker import capture_pre_state builder = BlockAccessListBuilder() tracker = StateChangeTracker(builder) - + mock_state = MagicMock() - address = Bytes20(b'\x01' * 20) - slot = Bytes32(b'\x02' * 32) + address = Bytes20(b"\x01" * 20) + slot = Bytes32(b"\x02" * 32) expected_value = U256(42) - + mock_get_storage.return_value = expected_value - + # First call should fetch from state value = capture_pre_state(tracker, address, slot, mock_state) assert value == expected_value mock_get_storage.assert_called_once_with(mock_state, address, slot) - + # Second call should use cache mock_get_storage.reset_mock() value2 = capture_pre_state(tracker, address, slot, mock_state) assert value2 == expected_value mock_get_storage.assert_not_called() - - @patch('ethereum.osaka.block_access_lists.tracker.capture_pre_state') - def test_tracker_storage_write_actual_change(self, mock_capture): + + @patch("ethereum.amsterdam.block_access_lists.tracker.capture_pre_state") + def test_tracker_storage_write_actual_change( + self, mock_capture: MagicMock + ) -> None: """Test tracking storage write with actual change.""" - from ethereum.osaka.block_access_lists.tracker import track_storage_write builder = BlockAccessListBuilder() tracker = StateChangeTracker(builder) tracker.current_block_access_index = 1 - + mock_state = MagicMock() - address = Bytes20(b'\x01' * 20) - slot = Bytes32(b'\x02' * 32) + address = Bytes20(b"\x01" * 20) + slot = Bytes32(b"\x02" * 32) old_value = U256(42) new_value = U256(100) - + mock_capture.return_value = old_value - + track_storage_write(tracker, address, slot, new_value, mock_state) - + # Should add storage write since value changed assert address in builder.accounts assert slot in builder.accounts[address].storage_changes assert len(builder.accounts[address].storage_changes[slot]) == 1 - + change = builder.accounts[address].storage_changes[slot][0] assert change.block_access_index == 1 assert change.new_value == new_value.to_be_bytes32() - - @patch('ethereum.osaka.block_access_lists.tracker.capture_pre_state') - def test_tracker_storage_write_no_change(self, mock_capture): + + @patch("ethereum.amsterdam.block_access_lists.tracker.capture_pre_state") + def test_tracker_storage_write_no_change( + self, mock_capture: MagicMock + ) -> None: """Test tracking storage write with no actual change.""" - from ethereum.osaka.block_access_lists.tracker import track_storage_write builder = BlockAccessListBuilder() tracker = StateChangeTracker(builder) tracker.current_block_access_index = 1 - + mock_state = MagicMock() - address = Bytes20(b'\x01' * 20) - slot = Bytes32(b'\x02' * 32) + address = Bytes20(b"\x01" * 20) + slot = Bytes32(b"\x02" * 32) same_value = U256(42) - + mock_capture.return_value = same_value - + track_storage_write(tracker, address, slot, same_value, mock_state) - + # Should add storage read since value didn't change assert address in builder.accounts assert slot in builder.accounts[address].storage_reads assert slot not in builder.accounts[address].storage_changes - - def test_tracker_balance_change(self): + + def test_tracker_balance_change(self) -> None: """Test tracking balance changes.""" - from ethereum.osaka.block_access_lists.tracker import track_balance_change builder = BlockAccessListBuilder() tracker = StateChangeTracker(builder) tracker.current_block_access_index = 2 - - mock_state = MagicMock() - address = Bytes20(b'\x01' * 20) + + address = Bytes20(b"\x01" * 20) new_balance = U256(1000) - - track_balance_change(tracker, address, new_balance, mock_state) - + + track_balance_change(tracker, address, new_balance) + assert address in builder.accounts assert len(builder.accounts[address].balance_changes) == 1 - + change = builder.accounts[address].balance_changes[0] assert change.block_access_index == 2 # Balance is stored as U256 per EIP-7928 assert change.post_balance == new_balance - - def test_tracker_nonce_change(self): + + def test_tracker_nonce_change(self) -> None: """Test tracking nonce changes.""" - from ethereum.osaka.block_access_lists.tracker import track_nonce_change builder = BlockAccessListBuilder() tracker = StateChangeTracker(builder) tracker.current_block_access_index = 3 - - mock_state = MagicMock() - address = Bytes20(b'\x01' * 20) + + address = Bytes20(b"\x01" * 20) new_nonce = U64(10) - - track_nonce_change(tracker, address, new_nonce, mock_state) - + + track_nonce_change(tracker, address, Uint(new_nonce)) + assert address in builder.accounts assert len(builder.accounts[address].nonce_changes) == 1 - + change = builder.accounts[address].nonce_changes[0] assert change.block_access_index == 3 assert change.new_nonce == new_nonce - - def test_tracker_code_change(self): + + def test_tracker_code_change(self) -> None: """Test tracking code changes.""" - from ethereum.osaka.block_access_lists.tracker import track_code_change builder = BlockAccessListBuilder() tracker = StateChangeTracker(builder) tracker.current_block_access_index = 1 - - mock_state = MagicMock() - address = Bytes20(b'\x01' * 20) - new_code = Bytes(b'\x60\x80\x60\x40') - - track_code_change(tracker, address, new_code, mock_state) - + + address = Bytes20(b"\x01" * 20) + new_code = Bytes(b"\x60\x80\x60\x40") + + track_code_change(tracker, address, new_code) + assert address in builder.accounts assert len(builder.accounts[address].code_changes) == 1 - + change = builder.accounts[address].code_changes[0] assert change.block_access_index == 1 assert change.new_code == new_code @@ -338,152 +339,205 @@ def test_tracker_code_change(self): class TestBALIntegration: """Test BAL integration with block execution.""" - - def test_system_contract_indices(self): + + def test_system_contract_indices(self) -> None: """Test that system contracts use block_access_index 0.""" builder = BlockAccessListBuilder() - + # Simulate pre-execution system contract changes - beacon_roots_addr = Bytes20(b'\x00' * 19 + b'\x02') - history_addr = Bytes20(b'\x00' * 19 + b'\x35') - + beacon_roots_addr = Bytes20(b"\x00" * 19 + b"\x02") + history_addr = Bytes20(b"\x00" * 19 + b"\x35") + # These should use index 0 - add_storage_write(builder, beacon_roots_addr, Bytes32(b'\x00' * 32), BlockAccessIndex(0), Bytes32(b'\x01' * 32)) - add_storage_write(builder, history_addr, Bytes32(b'\x00' * 32), BlockAccessIndex(0), Bytes32(b'\x02' * 32)) - + add_storage_write( + builder, + beacon_roots_addr, + Bytes32(b"\x00" * 32), + BlockAccessIndex(0), + Bytes32(b"\x01" * 32), + ) + add_storage_write( + builder, + history_addr, + Bytes32(b"\x00" * 32), + BlockAccessIndex(0), + Bytes32(b"\x02" * 32), + ) + block_access_list = build(builder) - + for account in block_access_list.account_changes: if account.address in [beacon_roots_addr, history_addr]: for slot_changes in account.storage_changes: for change in slot_changes.changes: assert change.block_access_index == 0 - - def test_transaction_indices(self): + + def test_transaction_indices(self) -> None: """Test that transactions use indices 1 to len(transactions).""" builder = BlockAccessListBuilder() - + # Simulate 3 transactions for tx_num in range(1, 4): - address = Bytes20(tx_num.to_bytes(20, 'big')) + address = Bytes20(tx_num.to_bytes(20, "big")) # Transactions should use indices 1, 2, 3 - add_balance_change(builder, address, BlockAccessIndex(tx_num), Bytes(b'\x00' * 16)) - + add_balance_change( + builder, address, BlockAccessIndex(tx_num), U256(0) + ) + block_access_list = build(builder) - + assert len(block_access_list.account_changes) == 3 for i, account in enumerate(block_access_list.account_changes): assert len(account.balance_changes) == 1 assert account.balance_changes[0].block_access_index == i + 1 - - def test_post_execution_index(self): + + def test_post_execution_index(self) -> None: """Test that post-execution changes use index len(transactions) + 1.""" builder = BlockAccessListBuilder() num_transactions = 5 - + # Simulate withdrawal (post-execution) - withdrawal_addr = Bytes20(b'\xff' * 20) + withdrawal_addr = Bytes20(b"\xff" * 20) post_exec_index = num_transactions + 1 - - add_balance_change(builder, withdrawal_addr, BlockAccessIndex(post_exec_index), Bytes(b'\x00' * 16)) - + + add_balance_change( + builder, + withdrawal_addr, + BlockAccessIndex(post_exec_index), + U256(0), + ) + block_access_list = build(builder) - + for account in block_access_list.account_changes: if account.address == withdrawal_addr: assert len(account.balance_changes) == 1 - assert account.balance_changes[0].block_access_index == post_exec_index - - def test_mixed_indices_ordering(self): + assert ( + account.balance_changes[0].block_access_index + == post_exec_index + ) + + def test_mixed_indices_ordering(self) -> None: """Test that mixed indices are properly ordered in the BAL.""" builder = BlockAccessListBuilder() - address = Bytes20(b'\x01' * 20) - + address = Bytes20(b"\x01" * 20) + # Add changes with different indices (out of order) - add_balance_change(builder, address, BlockAccessIndex(3), Bytes(b'\x03' * 16)) - add_balance_change(builder, address, BlockAccessIndex(1), Bytes(b'\x01' * 16)) - add_balance_change(builder, address, BlockAccessIndex(2), Bytes(b'\x02' * 16)) - add_balance_change(builder, address, BlockAccessIndex(0), Bytes(b'\x00' * 16)) - + add_balance_change( + builder, address, BlockAccessIndex(3), U256(0x03030303) + ) + add_balance_change( + builder, address, BlockAccessIndex(1), U256(0x01010101) + ) + add_balance_change( + builder, address, BlockAccessIndex(2), U256(0x02020202) + ) + add_balance_change(builder, address, BlockAccessIndex(0), U256(0)) + block_access_list = build(builder) - + assert len(block_access_list.account_changes) == 1 account = block_access_list.account_changes[0] assert len(account.balance_changes) == 4 - + # Should be sorted by block_access_index for i in range(4): assert account.balance_changes[i].block_access_index == i - assert account.balance_changes[i].post_balance == bytes([i]) * 16 + expected = ( + U256(0) + if i == 0 + else U256(int.from_bytes(bytes([i]) * 4, "big")) + ) + assert account.balance_changes[i].post_balance == expected class TestRLPEncoding: """Test RLP encoding of BAL structures.""" - - def test_rlp_encoding_import(self): + + def test_rlp_encoding_import(self) -> None: """Test that RLP encoding utilities can be imported.""" - from ethereum.osaka.block_access_lists import rlp_encode_block_access_list, compute_block_access_list_hash + from ethereum.amsterdam.block_access_lists import ( + compute_block_access_list_hash, + rlp_encode_block_access_list, + ) + assert rlp_encode_block_access_list is not None assert compute_block_access_list_hash is not None - - def test_rlp_encode_simple_bal(self): + + def test_rlp_encode_simple_bal(self) -> None: """Test RLP encoding of a simple BAL.""" - from ethereum.osaka.block_access_lists import rlp_encode_block_access_list - + from ethereum.amsterdam.block_access_lists import ( + rlp_encode_block_access_list, + ) + builder = BlockAccessListBuilder() - address = Bytes20(b'\x01' * 20) - - add_balance_change(builder, address, BlockAccessIndex(1), Bytes(b'\x00' * 16)) - + address = Bytes20(b"\x01" * 20) + + add_balance_change(builder, address, BlockAccessIndex(1), U256(0)) + block_access_list = build(builder) encoded = rlp_encode_block_access_list(block_access_list) - + # Should produce valid RLP bytes assert isinstance(encoded, (bytes, Bytes)) assert len(encoded) > 0 - - def test_bal_hash_computation(self): + + def test_bal_hash_computation(self) -> None: """Test BAL hash computation using RLP.""" - from ethereum.osaka.block_access_lists import compute_block_access_list_hash - + from ethereum.amsterdam.block_access_lists import ( + compute_block_access_list_hash, + ) + builder = BlockAccessListBuilder() - address = Bytes20(b'\x01' * 20) - - add_storage_write(builder, address, Bytes32(b'\x02' * 32), BlockAccessIndex(1), Bytes32(b'\x03' * 32)) - + address = Bytes20(b"\x01" * 20) + + add_storage_write( + builder, + address, + Bytes32(b"\x02" * 32), + BlockAccessIndex(1), + Bytes32(b"\x03" * 32), + ) + block_access_list = build(builder) hash_val = compute_block_access_list_hash(block_access_list) - + # Should produce a 32-byte hash assert len(hash_val) == 32 - + # Same BAL should produce same hash hash_val2 = compute_block_access_list_hash(block_access_list) assert hash_val == hash_val2 - - def test_rlp_encode_complex_bal(self): + + def test_rlp_encode_complex_bal(self) -> None: """Test RLP encoding of a complex BAL with multiple change types.""" - from ethereum.osaka.block_access_lists import rlp_encode_block_access_list - + from ethereum.amsterdam.block_access_lists import ( + rlp_encode_block_access_list, + ) + builder = BlockAccessListBuilder() - + # Add various types of changes - address = Bytes20(b'\x01' * 20) - slot = Bytes32(b'\x02' * 32) - + address = Bytes20(b"\x01" * 20) + slot = Bytes32(b"\x02" * 32) + # Pre-execution (index 0) - add_storage_write(builder, address, slot, BlockAccessIndex(0), Bytes32(b'\x03' * 32)) - + add_storage_write( + builder, address, slot, BlockAccessIndex(0), Bytes32(b"\x03" * 32) + ) + # Transaction (index 1) - add_balance_change(builder, address, BlockAccessIndex(1), Bytes(b'\x00' * 16)) + add_balance_change(builder, address, BlockAccessIndex(1), U256(0)) add_nonce_change(builder, address, BlockAccessIndex(1), U64(1)) - + # Post-execution (index 2) - add_code_change(builder, address, BlockAccessIndex(2), Bytes(b'\x60\x80')) - + add_code_change( + builder, address, BlockAccessIndex(2), Bytes(b"\x60\x80") + ) + block_access_list = build(builder) encoded = rlp_encode_block_access_list(block_access_list) - + # Should produce valid RLP bytes assert isinstance(encoded, (bytes, Bytes)) assert len(encoded) > 0 @@ -491,61 +545,67 @@ def test_rlp_encode_complex_bal(self): class TestEdgeCases: """Test edge cases and error handling.""" - - def test_empty_bal(self): + + def test_empty_bal(self) -> None: """Test building an empty BAL.""" builder = BlockAccessListBuilder() block_access_list = build(builder) - + assert isinstance(block_access_list, BlockAccessList) assert len(block_access_list.account_changes) == 0 - - def test_multiple_changes_same_slot(self): + + def test_multiple_changes_same_slot(self) -> None: """Test multiple changes to the same storage slot.""" builder = BlockAccessListBuilder() - address = Bytes20(b'\x01' * 20) - slot = Bytes32(b'\x02' * 32) - + address = Bytes20(b"\x01" * 20) + slot = Bytes32(b"\x02" * 32) + # Multiple writes to same slot at different indices - add_storage_write(builder, address, slot, BlockAccessIndex(0), Bytes32(b'\x00' * 32)) - add_storage_write(builder, address, slot, BlockAccessIndex(1), Bytes32(b'\x01' * 32)) - add_storage_write(builder, address, slot, BlockAccessIndex(2), Bytes32(b'\x02' * 32)) - + add_storage_write( + builder, address, slot, BlockAccessIndex(0), Bytes32(b"\x00" * 32) + ) + add_storage_write( + builder, address, slot, BlockAccessIndex(1), Bytes32(b"\x01" * 32) + ) + add_storage_write( + builder, address, slot, BlockAccessIndex(2), Bytes32(b"\x02" * 32) + ) + block_access_list = build(builder) - + assert len(block_access_list.account_changes) == 1 account = block_access_list.account_changes[0] assert len(account.storage_changes) == 1 - + slot_changes = account.storage_changes[0] assert slot_changes.slot == slot assert len(slot_changes.changes) == 3 - + # Changes should be sorted by index for i in range(3): assert slot_changes.changes[i].block_access_index == i - - def test_max_code_changes_constant(self): + + def test_max_code_changes_constant(self) -> None: """Test that MAX_CODE_CHANGES constant is available.""" assert MAX_CODE_CHANGES == 1 - - def test_address_sorting(self): + + def test_address_sorting(self) -> None: """Test that addresses are sorted lexicographically in BAL.""" builder = BlockAccessListBuilder() - + # Add addresses in reverse order addresses = [ - Bytes20(b'\xff' * 20), - Bytes20(b'\xaa' * 20), - Bytes20(b'\x11' * 20), - Bytes20(b'\x00' * 20), + Bytes20(b"\xff" * 20), + Bytes20(b"\xaa" * 20), + Bytes20(b"\x11" * 20), + Bytes20(b"\x00" * 20), ] - + for addr in addresses: add_touched_account(builder, addr) - + block_access_list = build(builder) - + # Should be sorted lexicographically sorted_addresses = sorted(addresses) for i, account in enumerate(block_access_list.account_changes): @@ -553,4 +613,4 @@ def test_address_sorting(self): if __name__ == "__main__": - pytest.main([__file__, "-v"]) \ No newline at end of file + pytest.main([__file__, "-v"]) diff --git a/tests/amsterdam/test_rlp.py b/tests/amsterdam/test_rlp.py new file mode 100644 index 0000000000..ae9fa85893 --- /dev/null +++ b/tests/amsterdam/test_rlp.py @@ -0,0 +1,178 @@ +import pytest +from ethereum_rlp import rlp +from ethereum_types.bytes import Bytes, Bytes0, Bytes8, Bytes32 +from ethereum_types.numeric import U64, U256, Uint + +from ethereum.amsterdam.blocks import Block, Header, Log, Receipt, Withdrawal +from ethereum.amsterdam.rlp_types import BlockAccessList +from ethereum.amsterdam.transactions import ( + Access, + AccessListTransaction, + FeeMarketTransaction, + LegacyTransaction, + Transaction, + decode_transaction, + encode_transaction, +) +from ethereum.amsterdam.utils.hexadecimal import hex_to_address +from ethereum.crypto.hash import keccak256 +from ethereum.utils.hexadecimal import hex_to_bytes256 + +hash1 = keccak256(b"foo") +hash2 = keccak256(b"bar") +hash3 = keccak256(b"baz") +hash4 = keccak256(b"foobar") +hash5 = keccak256(b"quux") +hash6 = keccak256(b"foobarbaz") +hash7 = keccak256(b"quuxbaz") + +address1 = hex_to_address("0x00000000219ab540356cbb839cbe05303d7705fa") +address2 = hex_to_address("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") + +bloom = hex_to_bytes256( + "0x886480c00200620d84180d0470000c503081160044d05015808" + "0037401107060120040105281100100104500414203040a208003" + "4814200610da1208a638d16e440c024880800301e1004c2b02285" + "0602000084c3249a0c084569c90c2002001586241041e8004035a" + "4400a0100938001e041180083180b0340661372060401428c0200" + "87410402b9484028100049481900c08034864314688d001548c30" + "00828e542284180280006402a28a0264da00ac223004006209609" + "83206603200084040122a4739080501251542082020a4087c0002" + "81c08800898d0900024047380000127038098e090801080000429" + "0c84201661040200201c0004b8490ad588804" +) + +legacy_transaction = LegacyTransaction( + U256(1), + Uint(2), + Uint(3), + Bytes0(), + U256(4), + Bytes(b"foo"), + U256(27), + U256(5), + U256(6), +) + +access_list_transaction = AccessListTransaction( + U64(1), + U256(1), + Uint(2), + Uint(3), + Bytes0(), + U256(4), + Bytes(b"bar"), + ( + Access(account=address1, slots=(hash1, hash2)), + Access(account=address2, slots=()), + ), + U256(27), + U256(5), + U256(6), +) + +transaction_1559 = FeeMarketTransaction( + U64(1), + U256(1), + Uint(7), + Uint(2), + Uint(3), + Bytes0(), + U256(4), + Bytes(b"bar"), + ( + Access(account=address1, slots=(hash1, hash2)), + Access(account=address2, slots=()), + ), + U256(27), + U256(5), + U256(6), +) + +withdrawal = Withdrawal(U64(0), U64(1), address1, U256(2)) + + +header = Header( + parent_hash=hash1, + ommers_hash=hash2, + coinbase=address1, + state_root=hash3, + transactions_root=hash4, + receipt_root=hash5, + bloom=bloom, + difficulty=Uint(1), + number=Uint(2), + gas_limit=Uint(3), + gas_used=Uint(4), + timestamp=U256(5), + extra_data=Bytes(b"foobar"), + prev_randao=Bytes32(b"1234567890abcdef1234567890abcdef"), + nonce=Bytes8(b"12345678"), + base_fee_per_gas=Uint(6), + withdrawals_root=hash6, + parent_beacon_block_root=Bytes32(b"1234567890abcdef1234567890abcdef"), + blob_gas_used=U64(7), + excess_blob_gas=U64(8), + requests_hash=hash7, + bal_hash=hash1, # Added missing bal_hash +) + +block = Block( + header=header, + block_access_list=BlockAccessList( + account_changes=() + ), # Added missing block_access_list + transactions=( + encode_transaction(legacy_transaction), + encode_transaction(access_list_transaction), + encode_transaction(transaction_1559), + ), + ommers=(), + withdrawals=(withdrawal,), +) + +log1 = Log( + address=address1, + topics=(hash1, hash2), + data=Bytes(b"foobar"), +) + +log2 = Log( + address=address1, + topics=(hash1,), + data=Bytes(b"quux"), +) + +receipt = Receipt( + succeeded=True, + cumulative_gas_used=Uint(1), + bloom=bloom, + logs=(log1, log2), +) + + +@pytest.mark.parametrize( + "rlp_object", + [ + legacy_transaction, + access_list_transaction, + transaction_1559, + header, + block, + log1, + log2, + receipt, + withdrawal, + ], +) +def test_cancun_rlp(rlp_object: rlp.Extended) -> None: + encoded = rlp.encode(rlp_object) + assert rlp.decode_to(type(rlp_object), encoded) == rlp_object + + +@pytest.mark.parametrize( + "tx", [legacy_transaction, access_list_transaction, transaction_1559] +) +def test_transaction_encoding(tx: Transaction) -> None: + encoded = encode_transaction(tx) + assert decode_transaction(encoded) == tx diff --git a/tests/osaka/conftest.py b/tests/osaka/conftest.py deleted file mode 100644 index ed5fd82cf9..0000000000 --- a/tests/osaka/conftest.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Minimal conftest for osaka BAL tests. -""" - -import pytest - - -def pytest_configure(config): - """Configure custom markers.""" - config.addinivalue_line("markers", "bal: mark test as BAL-related") - config.addinivalue_line("markers", "integration: mark test as integration test") \ No newline at end of file diff --git a/whitelist.txt b/whitelist.txt index 5e5868bb3e..ea1e5654d7 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -507,3 +507,12 @@ master whitelist AccountT uint + +bal +BAL +slot1 +slot2 +lexicographically +uint16 +uint128 +630m From b93d57e5e8f329f239ed9ab9ec06658e96ea7aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= Date: Mon, 8 Sep 2025 07:24:40 +0200 Subject: [PATCH 04/18] correct system contract addresses --- tests/amsterdam/test_bal_implementation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/amsterdam/test_bal_implementation.py b/tests/amsterdam/test_bal_implementation.py index 8d0db99a4a..df421806cc 100644 --- a/tests/amsterdam/test_bal_implementation.py +++ b/tests/amsterdam/test_bal_implementation.py @@ -345,8 +345,8 @@ def test_system_contract_indices(self) -> None: builder = BlockAccessListBuilder() # Simulate pre-execution system contract changes - beacon_roots_addr = Bytes20(b"\x00" * 19 + b"\x02") - history_addr = Bytes20(b"\x00" * 19 + b"\x35") + beacon_roots_addr = Bytes20(bytes.fromhex("000F3df6D732807Ef1319fB7B8bB8522d0Beac02")) + history_addr = Bytes20(bytes.fromhex("0000F90827F1C53a10cb7A02335B175320002935")) # These should use index 0 add_storage_write( From 3ebec14ef61a93a5a6474ab17dca089054e9cd7e Mon Sep 17 00:00:00 2001 From: Guruprasad Kamath Date: Tue, 9 Sep 2025 14:54:50 +0200 Subject: [PATCH 05/18] move state change tracker to State --- .../amsterdam/block_access_lists/tracker.py | 4 +- src/ethereum/forks/amsterdam/state.py | 31 ++++ .../evm_tools/loaders/fork_loader.py | 19 +++ .../evm_tools/t8n/__init__.py | 133 ++++++------------ .../evm_tools/t8n/t8n_types.py | 16 +-- 5 files changed, 98 insertions(+), 105 deletions(-) diff --git a/src/ethereum/amsterdam/block_access_lists/tracker.py b/src/ethereum/amsterdam/block_access_lists/tracker.py index 34f7aa95f0..fb27fe3146 100644 --- a/src/ethereum/amsterdam/block_access_lists/tracker.py +++ b/src/ethereum/amsterdam/block_access_lists/tracker.py @@ -64,7 +64,7 @@ class StateChangeTracker: from the beginning of the current transaction. """ - current_block_access_index: int = 0 + current_block_access_index: Uint = Uint(0) """ The current block access index (0 for pre-execution, 1..n for transactions, n+1 for post-execution). @@ -72,7 +72,7 @@ class StateChangeTracker: def set_transaction_index( - tracker: StateChangeTracker, block_access_index: int + tracker: StateChangeTracker, block_access_index: Uint ) -> None: """ Set the current block access index for tracking changes. diff --git a/src/ethereum/forks/amsterdam/state.py b/src/ethereum/forks/amsterdam/state.py index 7cec40084d..8d7a3504e8 100644 --- a/src/ethereum/forks/amsterdam/state.py +++ b/src/ethereum/forks/amsterdam/state.py @@ -24,6 +24,13 @@ from ethereum_types.frozen import modify from ethereum_types.numeric import U256, Uint +from .block_access_lists.builder import BlockAccessListBuilder +from .block_access_lists.tracker import ( + StateChangeTracker, + track_balance_change, + track_code_change, + track_nonce_change, +) from .fork_types import EMPTY_ACCOUNT, Account, Address, Root from .trie import EMPTY_TRIE_ROOT, Trie, copy_trie, root, trie_get, trie_set @@ -47,6 +54,9 @@ class State: ] ] = field(default_factory=list) created_accounts: Set[Address] = field(default_factory=set) + change_tracker: StateChangeTracker = field( + default_factory=lambda: StateChangeTracker(BlockAccessListBuilder()) + ) @dataclass @@ -487,6 +497,15 @@ def increase_recipient_balance(recipient: Account) -> None: modify_state(state, sender_address, reduce_sender_balance) modify_state(state, recipient_address, increase_recipient_balance) + sender_new_balance = get_account(state, sender_address).balance + recipient_new_balance = get_account(state, recipient_address).balance + + track_balance_change( + state.change_tracker, sender_address, U256(sender_new_balance) + ) + track_balance_change( + state.change_tracker, recipient_address, U256(recipient_new_balance) + ) def set_account_balance(state: State, address: Address, amount: U256) -> None: """ @@ -509,6 +528,7 @@ def set_balance(account: Account) -> None: modify_state(state, address, set_balance) + track_balance_change(state.change_tracker, address, amount) def increment_nonce(state: State, address: Address) -> None: """ @@ -528,6 +548,15 @@ def increase_nonce(sender: Account) -> None: modify_state(state, address, increase_nonce) + # Track nonce change for Block Access List + # (for ALL accounts and ALL nonce changes) + # This includes: + # - EOA senders (transaction nonce increments) + # - Contracts performing CREATE/CREATE2 + # - Deployed contracts + # - EIP-7702 authorities + account = get_account(state, address) + track_nonce_change(state.change_tracker, address, account.nonce) def set_code(state: State, address: Address, code: Bytes) -> None: """ @@ -550,6 +579,8 @@ def write_code(sender: Account) -> None: modify_state(state, address, write_code) + track_code_change(state.change_tracker, address, code) + def get_storage_original(state: State, address: Address, key: Bytes32) -> U256: """ 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 dfea5efd11..1cc49c0fc5 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -124,6 +124,25 @@ def signing_hash_155(self) -> Any: """signing_hash_155 function of the fork""" return self._module("transactions").signing_hash_155 + @property + def build(self) -> Any: + """build function of the fork""" + return self._module("block_access_lists").build + + @property + def compute_block_access_list_hash(self) -> Any: + """compute_block_access_list_hash function of the fork""" + return ( + self._module("block_access_lists").compute_block_access_list_hash + ) + + @property + def set_transaction_index(self) -> Any: + """set_transaction_index function of the fork""" + return ( + self._module("block_access_lists").set_transaction_index + ) + @property def signing_hash_2930(self) -> Any: """signing_hash_2930 function of the fork""" diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index 96d0c89519..cdb24194cc 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -9,16 +9,9 @@ from typing import Any, Final, TextIO, Type, TypeVar from ethereum_rlp import rlp -from ethereum_types.numeric import U64, U256, Uint +from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum import trace -from ethereum.forks.amsterdam.block_access_lists import ( - StateChangeTracker, - set_transaction_index, -) -from ethereum.amsterdam.block_access_lists.tracker import ( - finalize_transaction_changes, -) from ethereum.exceptions import EthereumException, InvalidBlock from ethereum_spec_tools.forks import Hardfork @@ -244,24 +237,12 @@ def run_state_test(self) -> Any: if len(self.txs.transactions) > 0: tx = self.txs.transactions[0] try: - # Only pass change_tracker for Amsterdam and later - if self.fork.is_after_fork("ethereum.forks.amsterdam"): - self.fork.process_transaction( - block_env=block_env, - block_output=block_output, - tx=tx, - index=Uint(0), - change_tracker=StateChangeTracker( - block_output.block_access_list_builder - ), - ) - else: - self.fork.process_transaction( - block_env=block_env, - block_output=block_output, - tx=tx, - index=Uint(0), - ) + self.fork.process_transaction( + block_env=block_env, + block_output=block_output, + tx=tx, + index=Uint(0), + ) except EthereumException as e: self.txs.rejected_txs[0] = f"Failed transaction: {e!r}" self.restore_state() @@ -271,61 +252,32 @@ def run_state_test(self) -> Any: self.result.rejected = self.txs.rejected_txs def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: - bal_change_tracker = None if self.fork.is_after_fork("ethereum.forks.amsterdam"): - bal_change_tracker = StateChangeTracker( - block_output.block_access_list_builder + self.fork.set_transaction_index( + block_env.state.change_tracker, Uint(0) ) - # EIP-7928: Set transaction index for block access lists - # pre-execution system contracts use index 0 - set_transaction_index(bal_change_tracker, 0) - if self.fork.is_after_fork("ethereum.forks.prague"): - process_args = { - "block_env": block_env, - "target_address": self.fork.HISTORY_STORAGE_ADDRESS, - "data": block_env.block_hashes[-1], # The parent hash - } - if self.fork.is_after_fork("ethereum.forks.amsterdam"): - process_args["change_tracker"] = bal_change_tracker - self.fork.process_unchecked_system_transaction(**process_args) + self.fork.process_unchecked_system_transaction( + block_env=block_env, + target_address=self.fork.HISTORY_STORAGE_ADDRESS, + data=block_env.block_hashes[-1], # The parent hash + ) if self.fork.is_after_fork("ethereum.forks.cancun"): - process_args = { - "block_env": block_env, - "target_address": self.fork.BEACON_ROOTS_ADDRESS, - "data": block_env.parent_beacon_block_root, - } - if self.fork.is_after_fork("ethereum.forks.amsterdam"): - process_args["change_tracker"] = bal_change_tracker - self.fork.process_unchecked_system_transaction(**process_args) + self.fork.process_unchecked_system_transaction( + block_env=block_env, + target_address=self.fork.BEACON_ROOTS_ADDRESS, + data=block_env.parent_beacon_block_root, + ) for tx_index, (original_idx, tx) in enumerate(zip( self.txs.successfully_parsed, self.txs.transactions, strict=True, )): self.backup_state() try: - process_tx_args = [ - block_env, - block_output, - tx, - Uint(original_idx), - ] - - if self.fork.is_after_fork("ethereum.forks.amsterdam"): - process_tx_args.append(bal_change_tracker) - - assert bal_change_tracker is not None - # use 1...n for transaction indices - set_transaction_index(bal_change_tracker, tx_index + 1) - self.fork.process_transaction(*process_tx_args) - finalize_transaction_changes( - bal_change_tracker, - block_env.state, - ) - else: - self.fork.process_transaction(*process_tx_args) - + self.fork.process_transaction( + block_env, block_output, tx, Uint(tx_index) + ) except EthereumException as e: self.txs.rejected_txs[original_idx] = ( f"Failed transaction: {e!r}" @@ -335,6 +287,18 @@ def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: f"Transaction {original_idx} failed: {e!r}" ) + if self.fork.is_after_fork("ethereum.forks.amsterdam"): + assert block_env.state.change_tracker is not None + num_transactions = ulen( + [tx for tx in self.txs.successfully_parsed if tx] + ) + + # post-execution use n + 1 + post_execution_index = num_transactions + Uint(1) + self.fork.set_transaction_index( + block_env.state.change_tracker, post_execution_index + ) + if not self.fork.is_after_fork("ethereum.forks.paris"): if self.options.state_reward is None: self.pay_block_rewards(self.fork.BLOCK_REWARD, block_env) @@ -344,34 +308,17 @@ def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: ) if self.fork.is_after_fork("ethereum.forks.shanghai"): - process_withdrawal_args = [ + self.fork.process_withdrawals( block_env, block_output, self.env.withdrawals - ] - - if self.fork.is_after_fork("ethereum.forks.amsterdam"): - assert bal_change_tracker is not None - process_withdrawal_args.append(bal_change_tracker) - - self.fork.process_withdrawals(*process_withdrawal_args) + ) if self.fork.is_after_fork("ethereum.forks.prague"): - process_general_purpose_args = [block_env, block_output] - - if self.fork.is_after_fork("ethereum.forks.amsterdam"): - assert bal_change_tracker is not None - process_general_purpose_args.append(bal_change_tracker) - - self.fork.process_general_purpose_requests(*process_general_purpose_args) + self.fork.process_general_purpose_requests(block_env, block_output) if self.fork.is_after_fork("ethereum.forks.amsterdam"): - assert bal_change_tracker is not None - num_transactions = len( - [tx for tx in self.txs.successfully_parsed if tx] - ) - - # post-execution use n + 1 - post_execution_index = num_transactions + 1 - set_transaction_index(bal_change_tracker, post_execution_index) + block_output.block_access_list = self.fork.build( + block_env.state.change_tracker.block_access_list_builder + ) def run_blockchain_test(self) -> None: """ 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 bfa67c7a4d..9cceb69286 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -325,17 +325,13 @@ def update(self, t8n: "T8N", block_env: Any, block_output: Any) -> None: self.requests = block_output.requests self.requests_hash = t8n.fork.compute_requests_hash(self.requests) - if hasattr(block_output, "block_access_list_builder"): - from ethereum.amsterdam.block_access_lists import ( - build, - compute_block_access_list_hash, - ) - - bal = build(block_output.block_access_list_builder) - self.block_access_list = ( - bal # Store the BAL object directly, not RLP + if hasattr(block_output, "block_access_list"): + self.block_access_list = block_output.block_access_list + self.block_access_list_hash = ( + t8n.fork.compute_block_access_list_hash( + block_output.block_access_list + ) ) - self.block_access_list_hash = compute_block_access_list_hash(bal) def _bal_to_json(self, bal: Any) -> Any: """ From e8b05b4bb2aae55d12616277f0790902a541fa22 Mon Sep 17 00:00:00 2001 From: fselmo Date: Tue, 9 Sep 2025 12:17:09 -0600 Subject: [PATCH 06/18] Some remaining fixes due to large refactor in `forks/osaka`: - Move BALs from amsterdam -> forks/amsterdam - rename: build -> build_block_access_list - fix docc issues --- pyproject.toml | 2 +- .../amsterdam/block_access_lists/__init__.py | 4 +- .../amsterdam/block_access_lists/builder.py | 28 ++++++------ .../amsterdam/block_access_lists/rlp_utils.py | 8 ++-- .../amsterdam/block_access_lists/tracker.py | 12 ++--- src/ethereum/forks/amsterdam/blocks.py | 18 ++++++++ src/ethereum/forks/amsterdam/fork.py | 38 ++++++++++++++-- .../{ => forks}/amsterdam/rlp_types.py | 0 src/ethereum/forks/amsterdam/state.py | 42 +++++++++++++----- src/ethereum/forks/amsterdam/vm/__init__.py | 8 +++- .../amsterdam/vm/instructions/environment.py | 17 +++++-- .../amsterdam/vm/instructions/storage.py | 27 +++++++++++- .../forks/amsterdam/vm/instructions/system.py | 28 +++++++++--- .../evm_tools/loaders/fork_loader.py | 2 +- tests/amsterdam/test_bal_implementation.py | 44 +++++++++---------- tests/amsterdam/test_rlp.py | 8 ++-- 16 files changed, 206 insertions(+), 80 deletions(-) rename src/ethereum/{ => forks}/amsterdam/block_access_lists/__init__.py (95%) rename src/ethereum/{ => forks}/amsterdam/block_access_lists/builder.py (91%) rename src/ethereum/{ => forks}/amsterdam/block_access_lists/rlp_utils.py (96%) rename src/ethereum/{ => forks}/amsterdam/block_access_lists/tracker.py (95%) rename src/ethereum/{ => forks}/amsterdam/rlp_types.py (100%) diff --git a/pyproject.toml b/pyproject.toml index c5a0d93b6e..55775b710c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -514,7 +514,7 @@ ignore = [ "src/ethereum_spec_tools/evm_tools/t8n/evm_trace.py" = [ "N815" # The traces must use camel case in JSON property names ] -"src/ethereum/amsterdam/blocks.py" = [ +"src/ethereum/forks/amsterdam/blocks.py" = [ "E501" # Line too long - needed for long ref links ] diff --git a/src/ethereum/amsterdam/block_access_lists/__init__.py b/src/ethereum/forks/amsterdam/block_access_lists/__init__.py similarity index 95% rename from src/ethereum/amsterdam/block_access_lists/__init__.py rename to src/ethereum/forks/amsterdam/block_access_lists/__init__.py index 3155ea77f3..294a83ecae 100644 --- a/src/ethereum/amsterdam/block_access_lists/__init__.py +++ b/src/ethereum/forks/amsterdam/block_access_lists/__init__.py @@ -10,7 +10,7 @@ add_storage_read, add_storage_write, add_touched_account, - build, + build_block_access_list, ) from .rlp_utils import ( compute_block_access_list_hash, @@ -37,7 +37,7 @@ "add_storage_read", "add_storage_write", "add_touched_account", - "build", + "build_block_access_list", "compute_block_access_list_hash", "set_transaction_index", "rlp_encode_block_access_list", diff --git a/src/ethereum/amsterdam/block_access_lists/builder.py b/src/ethereum/forks/amsterdam/block_access_lists/builder.py similarity index 91% rename from src/ethereum/amsterdam/block_access_lists/builder.py rename to src/ethereum/forks/amsterdam/block_access_lists/builder.py index 90ccb58807..f61707b7fc 100644 --- a/src/ethereum/amsterdam/block_access_lists/builder.py +++ b/src/ethereum/forks/amsterdam/block_access_lists/builder.py @@ -13,7 +13,7 @@ 2. **Build Phase**: After block execution, the accumulated data is sorted and encoded into the final deterministic format. -[`BlockAccessList`]: ref:ethereum.amsterdam.rlp_types.BlockAccessList +[`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501 """ from dataclasses import dataclass, field @@ -86,7 +86,7 @@ class BlockAccessListBuilder: by address, field type, and transaction index to enable efficient reconstruction of state changes. - [`BlockAccessList`]: ref:ethereum.amsterdam.rlp_types.BlockAccessList + [`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501 """ accounts: Dict[Address, AccountData] = field(default_factory=dict) @@ -111,7 +111,7 @@ def ensure_account(builder: BlockAccessListBuilder, address: Address) -> None: The account address to ensure exists. [`AccountData`] : - ref:ethereum.amsterdam.block_access_lists.builder.AccountData + ref:ethereum.forks.amsterdam.block_access_lists.builder.AccountData """ if address not in builder.accounts: builder.accounts[address] = AccountData() @@ -255,8 +255,8 @@ def add_nonce_change( new_nonce : The new nonce value after the change. - [`CREATE`]: ref:ethereum.amsterdam.vm.instructions.system.create - [`CREATE2`]: ref:ethereum.amsterdam.vm.instructions.system.create2 + [`CREATE`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2 """ ensure_account(builder, address) @@ -303,8 +303,8 @@ def add_code_change( new_code : The deployed contract bytecode. - [`CREATE`]: ref:ethereum.amsterdam.vm.instructions.system.create - [`CREATE2`]: ref:ethereum.amsterdam.vm.instructions.system.create2 + [`CREATE`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2 """ ensure_account(builder, address) @@ -333,18 +333,20 @@ def add_touched_account( The account address that was accessed. [`EXTCODEHASH`] : - ref:ethereum.amsterdam.vm.instructions.environment.extcodehash + ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodehash [`BALANCE`] : - ref:ethereum.amsterdam.vm.instructions.environment.balance + ref:ethereum.forks.amsterdam.vm.instructions.environment.balance [`EXTCODESIZE`] : - ref:ethereum.amsterdam.vm.instructions.environment.extcodesize + ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodesize [`EXTCODECOPY`] : - ref:ethereum.amsterdam.vm.instructions.environment.extcodecopy + ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodecopy """ ensure_account(builder, address) -def build(builder: BlockAccessListBuilder) -> BlockAccessList: +def build_block_access_list( + builder: BlockAccessListBuilder +) -> BlockAccessList: """ Build the final [`BlockAccessList`] from accumulated changes. @@ -366,7 +368,7 @@ def build(builder: BlockAccessListBuilder) -> BlockAccessList: block_access_list : The final sorted and encoded block access list. - [`BlockAccessList`]: ref:ethereum.amsterdam.rlp_types.BlockAccessList + [`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501 """ account_changes_list = [] diff --git a/src/ethereum/amsterdam/block_access_lists/rlp_utils.py b/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py similarity index 96% rename from src/ethereum/amsterdam/block_access_lists/rlp_utils.py rename to src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py index c26e4a5e2b..a1d2346f64 100644 --- a/src/ethereum/amsterdam/block_access_lists/rlp_utils.py +++ b/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py @@ -68,7 +68,7 @@ def rlp_encode_block_access_list(block_access_list: BlockAccessList) -> Bytes: encoded : The complete RLP-encoded block access list. - [`BlockAccessList`]: ref:ethereum.amsterdam.rlp_types.BlockAccessList + [`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501 """ # Encode as a list of AccountChanges directly (not wrapped) account_changes_list = [] @@ -216,10 +216,12 @@ def validate_block_access_list_against_execution( # 4. If Block Access List builder provided, validate against it # by comparing hashes if block_access_list_builder is not None: - from .builder import build + from .builder import build_block_access_list # Build a Block Access List from the builder - expected_block_access_list = build(block_access_list_builder) + expected_block_access_list = build_block_access_list( + block_access_list_builder + ) # Compare hashes if compute_block_access_list_hash( diff --git a/src/ethereum/amsterdam/block_access_lists/tracker.py b/src/ethereum/forks/amsterdam/block_access_lists/tracker.py similarity index 95% rename from src/ethereum/amsterdam/block_access_lists/tracker.py rename to src/ethereum/forks/amsterdam/block_access_lists/tracker.py index fb27fe3146..fb66438390 100644 --- a/src/ethereum/amsterdam/block_access_lists/tracker.py +++ b/src/ethereum/forks/amsterdam/block_access_lists/tracker.py @@ -49,7 +49,7 @@ class StateChangeTracker: are recorded in the access list. [`BlockAccessListBuilder`]: - ref:ethereum.amsterdam.block_access_lists.builder.BlockAccessListBuilder + ref:ethereum.forks.amsterdam.block_access_lists.builder.BlockAccessListBuilder """ block_access_list_builder: BlockAccessListBuilder @@ -278,8 +278,8 @@ def track_nonce_change( state : The current execution state. - [`CREATE`]: ref:ethereum.amsterdam.vm.instructions.system.create - [`CREATE2`]: ref:ethereum.amsterdam.vm.instructions.system.create2 + [`CREATE`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2 """ track_address_access(tracker, address) add_nonce_change( @@ -309,8 +309,8 @@ def track_code_change( new_code : The deployed contract bytecode. - [`CREATE`]: ref:ethereum.amsterdam.vm.instructions.system.create - [`CREATE2`]: ref:ethereum.amsterdam.vm.instructions.system.create2 + [`CREATE`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create + [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2 """ track_address_access(tracker, address) add_code_change( @@ -327,7 +327,7 @@ def finalize_transaction_changes( """ Finalize changes for the current transaction. - This method is called at the end of each transaction execution. Currently + This method is called at the end of each transaction execution. Currently, a no-op as all tracking is done incrementally during execution, but provided for future extensibility. diff --git a/src/ethereum/forks/amsterdam/blocks.py b/src/ethereum/forks/amsterdam/blocks.py index 1177e4c32d..dfb0e2f1fa 100644 --- a/src/ethereum/forks/amsterdam/blocks.py +++ b/src/ethereum/forks/amsterdam/blocks.py @@ -19,6 +19,7 @@ from ethereum.crypto.hash import Hash32 from .fork_types import Address, Bloom, Root +from .rlp_types import BlockAccessList from .transactions import ( AccessListTransaction, BlobTransaction, @@ -241,6 +242,16 @@ class Header: [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 """ + bal_hash: Hash32 + """ + [SHA2-256] hash of the Block Access List containing all accounts and + storage locations accessed during block execution. Introduced in + [EIP-7928]. See [`compute_block_access_list_hash`][cbalh] for more + details. + [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 + [cbalh]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_utils.compute_block_access_list_hash # noqa: E501 + """ + @slotted_freezable @dataclass @@ -294,6 +305,13 @@ class Block: A tuple of withdrawals processed in this block. """ + block_access_list: BlockAccessList + """ + Block Access List containing all accounts and storage locations accessed + during block execution. Introduced in [EIP-7928]. + [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 + """ + @slotted_freezable @dataclass diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 03d2955f2a..d0d9b481d6 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -17,7 +17,7 @@ from ethereum_rlp import rlp from ethereum_types.bytes import Bytes -from ethereum_types.numeric import U64, U256, Uint +from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.crypto.hash import Hash32, keccak256 from ethereum.exceptions import ( @@ -30,6 +30,12 @@ ) from . import vm +from .block_access_lists.builder import build_block_access_list +from .block_access_lists.rlp_utils import compute_block_access_list_hash +from .block_access_lists.tracker import ( + set_transaction_index, + track_balance_change, +) from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt from .bloom import logs_bloom from .exceptions import ( @@ -243,6 +249,9 @@ def state_transition(chain: BlockChain, block: Block) -> None: 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 = compute_block_access_list_hash( + block_output.block_access_list + ) if block_output.block_gas_used != block.header.gas_used: raise InvalidBlock( @@ -262,6 +271,8 @@ def state_transition(chain: BlockChain, block: Block) -> None: raise InvalidBlock if requests_hash != block.header.requests_hash: raise InvalidBlock + if computed_block_access_list_hash != block.header.bal_hash: + raise InvalidBlock("Invalid block access list hash") chain.blocks.append(block) if len(chain.blocks) > 255: @@ -753,6 +764,10 @@ def apply_body( """ block_output = vm.BlockOutput() + # Set system transaction index for pre-execution system contracts + # EIP-7928: System contracts use bal_index 0 + set_transaction_index(block_env.state.change_tracker, Uint(0)) + process_unchecked_system_transaction( block_env=block_env, target_address=BEACON_ROOTS_ADDRESS, @@ -768,12 +783,19 @@ def apply_body( for i, tx in enumerate(map(decode_transaction, transactions)): process_transaction(block_env, block_output, tx, Uint(i)) + # EIP-7928: Post-execution uses bal_index len(transactions) + 1 + post_execution_index = ulen(transactions) + Uint(1) + set_transaction_index(block_env.state.change_tracker, post_execution_index) + 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.state.change_tracker.block_access_list_builder + ) return block_output @@ -832,8 +854,8 @@ def process_transaction( Execute a transaction against the provided environment. This function processes the actions needed to execute a transaction. - It decrements the sender's account after calculating the gas fee and - refunds them the proper amount after execution. Calling contracts, + 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. @@ -851,6 +873,9 @@ def process_transaction( index: Index of the transaction in the block. """ + # EIP-7928: Transactions use bal_index 1 to len(transactions) + set_transaction_index(block_env.state.change_tracker, index + Uint(1)) + trie_set( block_output.transactions_trie, rlp.encode(index), @@ -1010,6 +1035,13 @@ def increase_recipient_balance(recipient: Account) -> None: modify_state(block_env.state, wd.address, increase_recipient_balance) + # Track balance change for BAL + # (withdrawals are tracked as system contract changes) + new_balance = get_account(block_env.state, wd.address).balance + track_balance_change( + block_env.state.change_tracker, wd.address, U256(new_balance) + ) + if account_exists_and_is_empty(block_env.state, wd.address): destroy_account(block_env.state, wd.address) diff --git a/src/ethereum/amsterdam/rlp_types.py b/src/ethereum/forks/amsterdam/rlp_types.py similarity index 100% rename from src/ethereum/amsterdam/rlp_types.py rename to src/ethereum/forks/amsterdam/rlp_types.py diff --git a/src/ethereum/forks/amsterdam/state.py b/src/ethereum/forks/amsterdam/state.py index 8d7a3504e8..06995554c6 100644 --- a/src/ethereum/forks/amsterdam/state.py +++ b/src/ethereum/forks/amsterdam/state.py @@ -435,6 +435,33 @@ def account_has_storage(state: State, address: Address) -> bool: return address in state._storage_tries +def account_exists_and_is_empty(state: State, address: Address) -> bool: + """ + Checks if an account exists and has zero nonce, empty code and zero + balance. + + Parameters + ---------- + state: + The 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(state, address) + return ( + account is not None + and account.nonce == Uint(0) + and account.code == b"" + and account.balance == 0 + ) + + def is_account_alive(state: State, address: Address) -> bool: """ Check whether an account is both in the state and non-empty. @@ -459,20 +486,10 @@ def modify_state( state: State, 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. + Modify an `Account` in the `State`. """ set_account(state, address, modify(get_account(state, address), f)) - - account = get_account_optional(state, address) - account_exists_and_is_empty = ( - account is not None - and account.nonce == Uint(0) - and account.code == b"" - and account.balance == 0 - ) - - if account_exists_and_is_empty: + if account_exists_and_is_empty(state, address): destroy_account(state, address) @@ -507,6 +524,7 @@ def increase_recipient_balance(recipient: Account) -> None: state.change_tracker, recipient_address, U256(recipient_new_balance) ) + def set_account_balance(state: State, address: Address, amount: U256) -> None: """ Sets the balance of an account. diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index 033293a5fd..c93b6ff881 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -24,12 +24,11 @@ from ..blocks import Log, Receipt, Withdrawal from ..fork_types import Address, Authorization, VersionedHash +from ..rlp_types import BlockAccessList from ..state import State, TransientStorage from ..transactions import LegacyTransaction from ..trie import Trie -__all__ = ("Environment", "Evm", "Message") - @dataclass class BlockEnvironment: @@ -74,6 +73,8 @@ class BlockOutput: 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) @@ -90,6 +91,9 @@ class BlockOutput: ) blob_gas_used: U64 = U64(0) requests: List[Bytes] = field(default_factory=list) + block_access_list: BlockAccessList = field( + default_factory=lambda: BlockAccessList(account_changes=()) + ) @dataclass diff --git a/src/ethereum/forks/amsterdam/vm/instructions/environment.py b/src/ethereum/forks/amsterdam/vm/instructions/environment.py index 226b3d3bb3..c4a2c1f8a1 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/environment.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/environment.py @@ -18,6 +18,7 @@ from ethereum.crypto.hash import keccak256 from ethereum.utils.numeric import ceil32 +from ...block_access_lists.tracker import track_address_access from ...fork_types import EMPTY_ACCOUNT from ...state import get_account from ...utils.address import to_address_masked @@ -85,7 +86,9 @@ def balance(evm: Evm) -> None: # OPERATION # Non-existent accounts default to EMPTY_ACCOUNT, which has balance 0. - balance = get_account(evm.message.block_env.state, address).balance + state = evm.message.block_env.state + balance = get_account(state, address).balance + track_address_access(state.change_tracker, address) push(evm.stack, balance) @@ -351,7 +354,9 @@ def extcodesize(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - code = get_account(evm.message.block_env.state, address).code + state = evm.message.block_env.state + code = get_account(state, address).code + track_address_access(state.change_tracker, address) codesize = U256(len(code)) push(evm.stack, codesize) @@ -393,7 +398,9 @@ def extcodecopy(evm: Evm) -> None: # OPERATION evm.memory += b"\x00" * extend_memory.expand_by - code = get_account(evm.message.block_env.state, address).code + state = evm.message.block_env.state + code = get_account(state, address).code + track_address_access(state.change_tracker, address) value = buffer_read(code, code_start_index, size) memory_write(evm.memory, memory_start_index, value) @@ -479,7 +486,9 @@ def extcodehash(evm: Evm) -> None: charge_gas(evm, access_gas_cost) # OPERATION - account = get_account(evm.message.block_env.state, address) + state = evm.message.block_env.state + account = get_account(state, address) + track_address_access(state.change_tracker, address) if account == EMPTY_ACCOUNT: codehash = U256(0) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py index 65a0d5a9b6..ab5da8376f 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -13,6 +13,10 @@ """ from ethereum_types.numeric import Uint +from ...block_access_lists.tracker import ( + track_storage_read, + track_storage_write, +) from ...state import ( get_storage, get_storage_original, @@ -56,8 +60,16 @@ def sload(evm: Evm) -> None: charge_gas(evm, GAS_COLD_SLOAD) # OPERATION + state = evm.message.block_env.state value = get_storage( - evm.message.block_env.state, evm.message.current_target, key + state, evm.message.current_target, key + ) + + track_storage_read( + state.change_tracker, + evm.message.current_target, + key, + evm.message.block_env.state, ) push(evm.stack, value) @@ -126,6 +138,19 @@ def sstore(evm: Evm) -> None: charge_gas(evm, gas_cost) if evm.message.is_static: raise WriteInStaticContext + + # Track storage write BEFORE modifying state + # so we capture the correct pre-value + + track_storage_write( + state.change_tracker, + evm.message.current_target, + key, + new_value, + state, + ) + + # Now modify the storage set_storage(state, evm.message.current_target, key, new_value) # PROGRAM COUNTER diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index d7308821bd..5d0d0c9a5c 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -17,6 +17,7 @@ from ethereum.utils.numeric import ceil32 +from ...block_access_lists.tracker import track_address_access from ...fork_types import Address from ...state import ( account_has_code_or_nonce, @@ -77,6 +78,7 @@ def generic_create( STACK_DEPTH_LIMIT, process_create_message, ) + state = evm.message.block_env.state call_data = memory_read_bytes( evm.memory, memory_start_position, memory_size @@ -91,7 +93,7 @@ def generic_create( evm.return_data = b"" sender_address = evm.message.current_target - sender = get_account(evm.message.block_env.state, sender_address) + sender = get_account(state, sender_address) if ( sender.balance < endowment @@ -105,15 +107,19 @@ def generic_create( evm.accessed_addresses.add(contract_address) if account_has_code_or_nonce( - evm.message.block_env.state, contract_address - ) or account_has_storage(evm.message.block_env.state, contract_address): + state, contract_address + ) or account_has_storage(state, contract_address): increment_nonce( - evm.message.block_env.state, evm.message.current_target + state, + evm.message.current_target, ) push(evm.stack, U256(0)) return - increment_nonce(evm.message.block_env.state, evm.message.current_target) + increment_nonce( + state, + evm.message.current_target, + ) child_message = Message( block_env=evm.message.block_env, @@ -134,6 +140,9 @@ def generic_create( disable_precompiles=False, parent_evm=evm, ) + + track_address_access(state.change_tracker, contract_address) + child_evm = process_create_message(child_message) if child_evm.error: @@ -324,6 +333,9 @@ def generic_call( disable_precompiles=disable_precompiles, parent_evm=evm, ) + + track_address_access(evm.message.block_env.state.change_tracker, to) + child_evm = process_message(child_message) if child_evm.error: @@ -561,7 +573,11 @@ def selfdestruct(evm: Evm) -> None: if originator in evm.message.block_env.state.created_accounts: # If beneficiary is the same as originator, then # the ether is burnt. - set_account_balance(evm.message.block_env.state, originator, U256(0)) + set_account_balance( + evm.message.block_env.state, + originator, + U256(0), + ) evm.accounts_to_delete.add(originator) # HALT the execution 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 1cc49c0fc5..b3a60544fe 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -127,7 +127,7 @@ def signing_hash_155(self) -> Any: @property def build(self) -> Any: """build function of the fork""" - return self._module("block_access_lists").build + return self._module("block_access_lists").build_block_access_list @property def compute_block_access_list_hash(self) -> Any: diff --git a/tests/amsterdam/test_bal_implementation.py b/tests/amsterdam/test_bal_implementation.py index df421806cc..35cc7a6517 100644 --- a/tests/amsterdam/test_bal_implementation.py +++ b/tests/amsterdam/test_bal_implementation.py @@ -14,7 +14,7 @@ from ethereum_types.bytes import Bytes, Bytes20, Bytes32 from ethereum_types.numeric import U64, U256, Uint -from ethereum.amsterdam.block_access_lists import ( +from ethereum.forks.amsterdam.block_access_lists import ( BlockAccessListBuilder, StateChangeTracker, add_balance_change, @@ -23,9 +23,9 @@ add_storage_read, add_storage_write, add_touched_account, - build, + build_block_access_list, ) -from ethereum.amsterdam.block_access_lists.tracker import ( +from ethereum.forks.amsterdam.block_access_lists.tracker import ( capture_pre_state, set_transaction_index, track_balance_change, @@ -33,7 +33,7 @@ track_nonce_change, track_storage_write, ) -from ethereum.amsterdam.rlp_types import ( +from ethereum.forks.amsterdam.rlp_types import ( MAX_CODE_CHANGES, BlockAccessIndex, BlockAccessList, @@ -160,7 +160,7 @@ def test_bal_builder_build_complete(self) -> None: add_touched_account(builder, address2) # Build BAL - block_access_list = build(builder) + block_access_list = build_block_access_list(builder) assert isinstance(block_access_list, BlockAccessList) assert len(block_access_list.account_changes) == 2 @@ -204,7 +204,7 @@ def test_tracker_set_transaction_index(self) -> None: # Pre-storage cache should persist across transactions assert tracker.pre_storage_cache == {} - @patch("ethereum.amsterdam.state.get_storage") + @patch("ethereum.forks.amsterdam.state.get_storage") def test_tracker_capture_pre_state( self, mock_get_storage: MagicMock ) -> None: @@ -230,7 +230,7 @@ def test_tracker_capture_pre_state( assert value2 == expected_value mock_get_storage.assert_not_called() - @patch("ethereum.amsterdam.block_access_lists.tracker.capture_pre_state") + @patch("ethereum.forks.amsterdam.block_access_lists.tracker.capture_pre_state") def test_tracker_storage_write_actual_change( self, mock_capture: MagicMock ) -> None: @@ -258,7 +258,7 @@ def test_tracker_storage_write_actual_change( assert change.block_access_index == 1 assert change.new_value == new_value.to_be_bytes32() - @patch("ethereum.amsterdam.block_access_lists.tracker.capture_pre_state") + @patch("ethereum.forks.amsterdam.block_access_lists.tracker.capture_pre_state") def test_tracker_storage_write_no_change( self, mock_capture: MagicMock ) -> None: @@ -364,7 +364,7 @@ def test_system_contract_indices(self) -> None: Bytes32(b"\x02" * 32), ) - block_access_list = build(builder) + block_access_list = build_block_access_list(builder) for account in block_access_list.account_changes: if account.address in [beacon_roots_addr, history_addr]: @@ -384,7 +384,7 @@ def test_transaction_indices(self) -> None: builder, address, BlockAccessIndex(tx_num), U256(0) ) - block_access_list = build(builder) + block_access_list = build_block_access_list(builder) assert len(block_access_list.account_changes) == 3 for i, account in enumerate(block_access_list.account_changes): @@ -407,7 +407,7 @@ def test_post_execution_index(self) -> None: U256(0), ) - block_access_list = build(builder) + block_access_list = build_block_access_list(builder) for account in block_access_list.account_changes: if account.address == withdrawal_addr: @@ -434,7 +434,7 @@ def test_mixed_indices_ordering(self) -> None: ) add_balance_change(builder, address, BlockAccessIndex(0), U256(0)) - block_access_list = build(builder) + block_access_list = build_block_access_list(builder) assert len(block_access_list.account_changes) == 1 account = block_access_list.account_changes[0] @@ -456,7 +456,7 @@ class TestRLPEncoding: def test_rlp_encoding_import(self) -> None: """Test that RLP encoding utilities can be imported.""" - from ethereum.amsterdam.block_access_lists import ( + from ethereum.forks.amsterdam.block_access_lists import ( compute_block_access_list_hash, rlp_encode_block_access_list, ) @@ -466,7 +466,7 @@ def test_rlp_encoding_import(self) -> None: def test_rlp_encode_simple_bal(self) -> None: """Test RLP encoding of a simple BAL.""" - from ethereum.amsterdam.block_access_lists import ( + from ethereum.forks.amsterdam.block_access_lists import ( rlp_encode_block_access_list, ) @@ -475,7 +475,7 @@ def test_rlp_encode_simple_bal(self) -> None: add_balance_change(builder, address, BlockAccessIndex(1), U256(0)) - block_access_list = build(builder) + block_access_list = build_block_access_list(builder) encoded = rlp_encode_block_access_list(block_access_list) # Should produce valid RLP bytes @@ -484,7 +484,7 @@ def test_rlp_encode_simple_bal(self) -> None: def test_bal_hash_computation(self) -> None: """Test BAL hash computation using RLP.""" - from ethereum.amsterdam.block_access_lists import ( + from ethereum.forks.amsterdam.block_access_lists import ( compute_block_access_list_hash, ) @@ -499,7 +499,7 @@ def test_bal_hash_computation(self) -> None: Bytes32(b"\x03" * 32), ) - block_access_list = build(builder) + block_access_list = build_block_access_list(builder) hash_val = compute_block_access_list_hash(block_access_list) # Should produce a 32-byte hash @@ -511,7 +511,7 @@ def test_bal_hash_computation(self) -> None: def test_rlp_encode_complex_bal(self) -> None: """Test RLP encoding of a complex BAL with multiple change types.""" - from ethereum.amsterdam.block_access_lists import ( + from ethereum.forks.amsterdam.block_access_lists import ( rlp_encode_block_access_list, ) @@ -535,7 +535,7 @@ def test_rlp_encode_complex_bal(self) -> None: builder, address, BlockAccessIndex(2), Bytes(b"\x60\x80") ) - block_access_list = build(builder) + block_access_list = build_block_access_list(builder) encoded = rlp_encode_block_access_list(block_access_list) # Should produce valid RLP bytes @@ -549,7 +549,7 @@ class TestEdgeCases: def test_empty_bal(self) -> None: """Test building an empty BAL.""" builder = BlockAccessListBuilder() - block_access_list = build(builder) + block_access_list = build_block_access_list(builder) assert isinstance(block_access_list, BlockAccessList) assert len(block_access_list.account_changes) == 0 @@ -571,7 +571,7 @@ def test_multiple_changes_same_slot(self) -> None: builder, address, slot, BlockAccessIndex(2), Bytes32(b"\x02" * 32) ) - block_access_list = build(builder) + block_access_list = build_block_access_list(builder) assert len(block_access_list.account_changes) == 1 account = block_access_list.account_changes[0] @@ -604,7 +604,7 @@ def test_address_sorting(self) -> None: for addr in addresses: add_touched_account(builder, addr) - block_access_list = build(builder) + block_access_list = build_block_access_list(builder) # Should be sorted lexicographically sorted_addresses = sorted(addresses) diff --git a/tests/amsterdam/test_rlp.py b/tests/amsterdam/test_rlp.py index ae9fa85893..8985f4285a 100644 --- a/tests/amsterdam/test_rlp.py +++ b/tests/amsterdam/test_rlp.py @@ -3,9 +3,9 @@ from ethereum_types.bytes import Bytes, Bytes0, Bytes8, Bytes32 from ethereum_types.numeric import U64, U256, Uint -from ethereum.amsterdam.blocks import Block, Header, Log, Receipt, Withdrawal -from ethereum.amsterdam.rlp_types import BlockAccessList -from ethereum.amsterdam.transactions import ( +from ethereum.forks.amsterdam.blocks import Block, Header, Log, Receipt, Withdrawal +from ethereum.forks.amsterdam.rlp_types import BlockAccessList +from ethereum.forks.amsterdam.transactions import ( Access, AccessListTransaction, FeeMarketTransaction, @@ -14,7 +14,7 @@ decode_transaction, encode_transaction, ) -from ethereum.amsterdam.utils.hexadecimal import hex_to_address +from ethereum.forks.amsterdam.utils.hexadecimal import hex_to_address from ethereum.crypto.hash import keccak256 from ethereum.utils.hexadecimal import hex_to_bytes256 From dfc9c8e1ab8458922418e07c79a712f6b5ec8dfc Mon Sep 17 00:00:00 2001 From: fselmo Date: Fri, 19 Sep 2025 16:15:22 -0600 Subject: [PATCH 07/18] refactor: move rlp_utils to block_access_lists; bal -> block_access_lists --- pyproject.toml | 6 ++++++ .../amsterdam/block_access_lists/builder.py | 2 +- .../{ => block_access_lists}/rlp_types.py | 0 .../amsterdam/block_access_lists/rlp_utils.py | 2 +- .../amsterdam/block_access_lists/tracker.py | 2 +- src/ethereum/forks/amsterdam/blocks.py | 4 ++-- src/ethereum/forks/amsterdam/fork.py | 8 ++++---- src/ethereum/forks/amsterdam/vm/__init__.py | 2 +- src/ethereum/genesis.py | 4 ++-- .../evm_tools/loaders/fork_loader.py | 2 +- .../evm_tools/t8n/__init__.py | 6 +++--- .../evm_tools/t8n/t8n_types.py | 6 ++++-- tests/amsterdam/test_bal_implementation.py | 20 +++++++++---------- tests/amsterdam/test_rlp.py | 4 ++-- whitelist.txt | 2 -- 15 files changed, 38 insertions(+), 32 deletions(-) rename src/ethereum/forks/amsterdam/{ => block_access_lists}/rlp_types.py (100%) diff --git a/pyproject.toml b/pyproject.toml index 55775b710c..a2987aef0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -517,6 +517,12 @@ ignore = [ "src/ethereum/forks/amsterdam/blocks.py" = [ "E501" # Line too long - needed for long ref links ] + "src/ethereum/forks/amsterdam/block_access_lists/builder.py" = [ + "E501" # Line too long - needed for long ref links + ] +"src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py" = [ + "E501" # Line too long - needed for long ref links + ] [tool.ruff.lint.mccabe] # Set the maximum allowed cyclomatic complexity. C901 default is 10. diff --git a/src/ethereum/forks/amsterdam/block_access_lists/builder.py b/src/ethereum/forks/amsterdam/block_access_lists/builder.py index f61707b7fc..113d868563 100644 --- a/src/ethereum/forks/amsterdam/block_access_lists/builder.py +++ b/src/ethereum/forks/amsterdam/block_access_lists/builder.py @@ -23,7 +23,7 @@ from ethereum_types.numeric import U64, U256 from ..fork_types import Address -from ..rlp_types import ( +from .rlp_types import ( AccountChanges, BalanceChange, BlockAccessIndex, diff --git a/src/ethereum/forks/amsterdam/rlp_types.py b/src/ethereum/forks/amsterdam/block_access_lists/rlp_types.py similarity index 100% rename from src/ethereum/forks/amsterdam/rlp_types.py rename to src/ethereum/forks/amsterdam/block_access_lists/rlp_types.py diff --git a/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py b/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py index a1d2346f64..262a8f1d21 100644 --- a/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py +++ b/src/ethereum/forks/amsterdam/block_access_lists/rlp_utils.py @@ -24,8 +24,8 @@ from ethereum.crypto.hash import Hash32, keccak256 -from ..rlp_types import MAX_CODE_SIZE, MAX_TXS, BlockAccessList from .builder import BlockAccessListBuilder +from .rlp_types import MAX_CODE_SIZE, MAX_TXS, BlockAccessList def compute_block_access_list_hash( diff --git a/src/ethereum/forks/amsterdam/block_access_lists/tracker.py b/src/ethereum/forks/amsterdam/block_access_lists/tracker.py index fb66438390..7fe8735deb 100644 --- a/src/ethereum/forks/amsterdam/block_access_lists/tracker.py +++ b/src/ethereum/forks/amsterdam/block_access_lists/tracker.py @@ -22,7 +22,6 @@ from ethereum_types.numeric import U64, U256, Uint from ..fork_types import Address -from ..rlp_types import BlockAccessIndex from .builder import ( BlockAccessListBuilder, add_balance_change, @@ -32,6 +31,7 @@ add_storage_write, add_touched_account, ) +from .rlp_types import BlockAccessIndex if TYPE_CHECKING: from ..state import State # noqa: F401 diff --git a/src/ethereum/forks/amsterdam/blocks.py b/src/ethereum/forks/amsterdam/blocks.py index dfb0e2f1fa..95942c54e2 100644 --- a/src/ethereum/forks/amsterdam/blocks.py +++ b/src/ethereum/forks/amsterdam/blocks.py @@ -18,8 +18,8 @@ from ethereum.crypto.hash import Hash32 +from .block_access_lists.rlp_types import BlockAccessList from .fork_types import Address, Bloom, Root -from .rlp_types import BlockAccessList from .transactions import ( AccessListTransaction, BlobTransaction, @@ -242,7 +242,7 @@ class Header: [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 """ - bal_hash: Hash32 + block_access_list_hash: Hash32 """ [SHA2-256] hash of the Block Access List containing all accounts and storage locations accessed during block execution. Introduced in diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index d0d9b481d6..569d1c81ec 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -271,7 +271,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: raise InvalidBlock if requests_hash != block.header.requests_hash: raise InvalidBlock - if computed_block_access_list_hash != block.header.bal_hash: + if computed_block_access_list_hash != block.header.block_access_list_hash: raise InvalidBlock("Invalid block access list hash") chain.blocks.append(block) @@ -765,7 +765,7 @@ def apply_body( block_output = vm.BlockOutput() # Set system transaction index for pre-execution system contracts - # EIP-7928: System contracts use bal_index 0 + # EIP-7928: System contracts use block_access_index 0 set_transaction_index(block_env.state.change_tracker, Uint(0)) process_unchecked_system_transaction( @@ -783,7 +783,7 @@ def apply_body( for i, tx in enumerate(map(decode_transaction, transactions)): process_transaction(block_env, block_output, tx, Uint(i)) - # EIP-7928: Post-execution uses bal_index len(transactions) + 1 + # EIP-7928: Post-execution uses block_access_index len(transactions) + 1 post_execution_index = ulen(transactions) + Uint(1) set_transaction_index(block_env.state.change_tracker, post_execution_index) @@ -873,7 +873,7 @@ def process_transaction( index: Index of the transaction in the block. """ - # EIP-7928: Transactions use bal_index 1 to len(transactions) + # EIP-7928: Transactions use block_access_index 1 to len(transactions) set_transaction_index(block_env.state.change_tracker, index + Uint(1)) trie_set( diff --git a/src/ethereum/forks/amsterdam/vm/__init__.py b/src/ethereum/forks/amsterdam/vm/__init__.py index c93b6ff881..c9ac7cf04c 100644 --- a/src/ethereum/forks/amsterdam/vm/__init__.py +++ b/src/ethereum/forks/amsterdam/vm/__init__.py @@ -22,9 +22,9 @@ from ethereum.crypto.hash import Hash32 from ethereum.exceptions import EthereumException +from ..block_access_lists.rlp_types import BlockAccessList from ..blocks import Log, Receipt, Withdrawal from ..fork_types import Address, Authorization, VersionedHash -from ..rlp_types import BlockAccessList from ..state import State, TransientStorage from ..transactions import LegacyTransaction from ..trie import Trie diff --git a/src/ethereum/genesis.py b/src/ethereum/genesis.py index e11e9962b5..7f89975ce4 100644 --- a/src/ethereum/genesis.py +++ b/src/ethereum/genesis.py @@ -259,8 +259,8 @@ def add_genesis_block( if has_field(hardfork.Header, "requests_hash"): fields["requests_hash"] = Hash32(b"\0" * 32) - if has_field(hardfork.Header, "bal_hash"): - fields["bal_hash"] = Hash32(b"\0" * 32) + if has_field(hardfork.Header, "block_access_list_hash"): + fields["block_access_list_hash"] = Hash32(b"\0" * 32) genesis_header = hardfork.Header(**fields) 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 b3a60544fe..fac4de4d39 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -125,7 +125,7 @@ def signing_hash_155(self) -> Any: return self._module("transactions").signing_hash_155 @property - def build(self) -> Any: + def build_block_access_list(self) -> Any: """build function of the fork""" return self._module("block_access_lists").build_block_access_list diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index cdb24194cc..d5471b5b41 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -316,9 +316,9 @@ def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: self.fork.process_general_purpose_requests(block_env, block_output) if self.fork.is_after_fork("ethereum.forks.amsterdam"): - block_output.block_access_list = self.fork.build( - block_env.state.change_tracker.block_access_list_builder - ) + block_output.block_access_list = self.fork.build_block_access_list( + block_env.state.change_tracker.block_access_list_builder + ) def run_blockchain_test(self) -> None: """ 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 9cceb69286..3d3e0830ca 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -333,7 +333,7 @@ def update(self, t8n: "T8N", block_env: Any, block_output: Any) -> None: ) ) - def _bal_to_json(self, bal: Any) -> Any: + def _block_access_list_to_json(self, bal: Any) -> Any: """ Convert BlockAccessList to JSON format matching the Pydantic models. """ @@ -475,7 +475,9 @@ def to_json(self) -> Any: if self.block_access_list is not None: # Convert BAL to JSON format - data["blockAccessList"] = self._bal_to_json(self.block_access_list) + data["blockAccessList"] = self._block_access_list_to_json( + self.block_access_list + ) if self.block_access_list_hash is not None: data["blockAccessListHash"] = encode_to_hex( diff --git a/tests/amsterdam/test_bal_implementation.py b/tests/amsterdam/test_bal_implementation.py index 35cc7a6517..988c11dcc6 100644 --- a/tests/amsterdam/test_bal_implementation.py +++ b/tests/amsterdam/test_bal_implementation.py @@ -33,7 +33,7 @@ track_nonce_change, track_storage_write, ) -from ethereum.forks.amsterdam.rlp_types import ( +from ethereum.forks.amsterdam.block_access_lists.rlp_types import ( MAX_CODE_CHANGES, BlockAccessIndex, BlockAccessList, @@ -43,12 +43,12 @@ class TestBALCore: """Test core BAL functionality.""" - def test_bal_builder_initialization(self) -> None: + def test_block_access_list_builder_initialization(self) -> None: """Test BAL builder initializes correctly.""" builder = BlockAccessListBuilder() assert builder.accounts == {} - def test_bal_builder_add_storage_write(self) -> None: + def test_block_access_list_builder_add_storage_write(self) -> None: """Test adding storage writes to BAL builder.""" builder = BlockAccessListBuilder() address = Bytes20(b"\x01" * 20) @@ -65,7 +65,7 @@ def test_bal_builder_add_storage_write(self) -> None: assert change.block_access_index == 0 assert change.new_value == value - def test_bal_builder_add_storage_read(self) -> None: + def test_block_access_list_builder_add_storage_read(self) -> None: """Test adding storage reads to BAL builder.""" builder = BlockAccessListBuilder() address = Bytes20(b"\x01" * 20) @@ -76,7 +76,7 @@ def test_bal_builder_add_storage_read(self) -> None: assert address in builder.accounts assert slot in builder.accounts[address].storage_reads - def test_bal_builder_add_balance_change(self) -> None: + def test_block_access_list_builder_add_balance_change(self) -> None: """Test adding balance changes to BAL builder.""" builder = BlockAccessListBuilder() address = Bytes20(b"\x01" * 20) @@ -91,7 +91,7 @@ def test_bal_builder_add_balance_change(self) -> None: assert change.block_access_index == 0 assert change.post_balance == balance - def test_bal_builder_add_nonce_change(self) -> None: + def test_block_access_list_builder_add_nonce_change(self) -> None: """Test adding nonce changes to BAL builder.""" builder = BlockAccessListBuilder() address = Bytes20(b"\x01" * 20) @@ -106,7 +106,7 @@ def test_bal_builder_add_nonce_change(self) -> None: assert change.block_access_index == 0 assert change.new_nonce == U64(42) - def test_bal_builder_add_code_change(self) -> None: + def test_block_access_list_builder_add_code_change(self) -> None: """Test adding code changes to BAL builder.""" builder = BlockAccessListBuilder() address = Bytes20(b"\x01" * 20) @@ -121,7 +121,7 @@ def test_bal_builder_add_code_change(self) -> None: assert change.block_access_index == 0 assert change.new_code == code - def test_bal_builder_touched_account(self) -> None: + def test_block_access_list_builder_touched_account(self) -> None: """Test adding touched accounts without changes.""" builder = BlockAccessListBuilder() address = Bytes20(b"\x01" * 20) @@ -135,7 +135,7 @@ def test_bal_builder_touched_account(self) -> None: assert builder.accounts[address].nonce_changes == [] assert builder.accounts[address].code_changes == [] - def test_bal_builder_build_complete(self) -> None: + def test_block_access_list_builder_build_complete(self) -> None: """Test building a complete BlockAccessList.""" builder = BlockAccessListBuilder() @@ -482,7 +482,7 @@ def test_rlp_encode_simple_bal(self) -> None: assert isinstance(encoded, (bytes, Bytes)) assert len(encoded) > 0 - def test_bal_hash_computation(self) -> None: + def test_block_access_list_hash_computation(self) -> None: """Test BAL hash computation using RLP.""" from ethereum.forks.amsterdam.block_access_lists import ( compute_block_access_list_hash, diff --git a/tests/amsterdam/test_rlp.py b/tests/amsterdam/test_rlp.py index 8985f4285a..c5ef5108a1 100644 --- a/tests/amsterdam/test_rlp.py +++ b/tests/amsterdam/test_rlp.py @@ -4,7 +4,7 @@ from ethereum_types.numeric import U64, U256, Uint from ethereum.forks.amsterdam.blocks import Block, Header, Log, Receipt, Withdrawal -from ethereum.forks.amsterdam.rlp_types import BlockAccessList +from ethereum.forks.amsterdam.block_access_lists.rlp_types import BlockAccessList from ethereum.forks.amsterdam.transactions import ( Access, AccessListTransaction, @@ -114,7 +114,7 @@ blob_gas_used=U64(7), excess_blob_gas=U64(8), requests_hash=hash7, - bal_hash=hash1, # Added missing bal_hash + block_access_list_hash=hash1, # Added missing block_access_list_hash ) block = Block( diff --git a/whitelist.txt b/whitelist.txt index ea1e5654d7..1acb0cfe8f 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -508,8 +508,6 @@ whitelist AccountT uint -bal -BAL slot1 slot2 lexicographically From ec844605113af8114e1bd36cf1ebbe6593cf15e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= <51536394+nerolation@users.noreply.github.com> Date: Mon, 22 Sep 2025 02:43:36 +0200 Subject: [PATCH 08/18] fix zero-value transfer tracking (#6) * fix zero-value transfer tracking * fix reverted frame tracking * rename variables * fix missing addresses bug * fix: docs run & move imports to top of file --------- Co-authored-by: fselmo --- .../amsterdam/block_access_lists/__init__.py | 10 +- .../amsterdam/block_access_lists/tracker.py | 200 ++++++++++++++- src/ethereum/forks/amsterdam/fork.py | 13 +- .../forks/amsterdam/vm/interpreter.py | 15 ++ .../evm_tools/loaders/fork_loader.py | 6 +- .../evm_tools/t8n/__init__.py | 4 +- tests/amsterdam/test_bal_implementation.py | 234 +++++++++++++++++- 7 files changed, 456 insertions(+), 26 deletions(-) diff --git a/src/ethereum/forks/amsterdam/block_access_lists/__init__.py b/src/ethereum/forks/amsterdam/block_access_lists/__init__.py index 294a83ecae..856ab832bc 100644 --- a/src/ethereum/forks/amsterdam/block_access_lists/__init__.py +++ b/src/ethereum/forks/amsterdam/block_access_lists/__init__.py @@ -19,7 +19,10 @@ ) from .tracker import ( StateChangeTracker, - set_transaction_index, + begin_call_frame, + commit_call_frame, + rollback_call_frame, + set_block_access_index, track_address_access, track_balance_change, track_code_change, @@ -37,9 +40,12 @@ "add_storage_read", "add_storage_write", "add_touched_account", + "begin_call_frame", "build_block_access_list", + "commit_call_frame", "compute_block_access_list_hash", - "set_transaction_index", + "rollback_call_frame", + "set_block_access_index", "rlp_encode_block_access_list", "track_address_access", "track_balance_change", diff --git a/src/ethereum/forks/amsterdam/block_access_lists/tracker.py b/src/ethereum/forks/amsterdam/block_access_lists/tracker.py index 7fe8735deb..1ad068a604 100644 --- a/src/ethereum/forks/amsterdam/block_access_lists/tracker.py +++ b/src/ethereum/forks/amsterdam/block_access_lists/tracker.py @@ -16,7 +16,7 @@ """ from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Dict +from typing import TYPE_CHECKING, Dict, List, Set, Tuple from ethereum_types.bytes import Bytes, Bytes32 from ethereum_types.numeric import U64, U256, Uint @@ -37,6 +37,39 @@ from ..state import State # noqa: F401 +@dataclass +class CallFrameSnapshot: + """ + Snapshot of block access list state for a single call frame. + + Used to track changes within a call frame to enable proper handling + of reverts as specified in EIP-7928. + """ + + touched_addresses: Set[Address] = field(default_factory=set) + """Addresses touched during this call frame.""" + + storage_writes: Dict[Tuple[Address, Bytes32], U256] = field( + default_factory=dict + ) + """Storage writes made during this call frame.""" + + balance_changes: Set[Tuple[Address, BlockAccessIndex, U256]] = field( + default_factory=set + ) + """Balance changes made during this call frame.""" + + nonce_changes: Set[Tuple[Address, BlockAccessIndex, U64]] = field( + default_factory=set + ) + """Nonce changes made during this call frame.""" + + code_changes: Set[Tuple[Address, BlockAccessIndex, Bytes]] = field( + default_factory=set + ) + """Code changes made during this call frame.""" + + @dataclass class StateChangeTracker: """ @@ -70,16 +103,25 @@ class StateChangeTracker: 1..n for transactions, n+1 for post-execution). """ + call_frame_snapshots: List[CallFrameSnapshot] = field(default_factory=list) + """ + Stack of snapshots for nested call frames to handle reverts properly. + """ -def set_transaction_index( + +def set_block_access_index( tracker: StateChangeTracker, block_access_index: Uint ) -> None: """ Set the current block access index for tracking changes. Must be called before processing each transaction/system contract - to ensure changes - are associated with the correct block access index. + to ensure changes are associated with the correct block access index. + + Note: Block access indices differ from transaction indices: + - 0: Pre-execution (system contracts like beacon roots, block hashes) + - 1..n: Transactions (tx at index i gets block_access_index i+1) + - n+1: Post-execution (withdrawals, requests) Parameters ---------- @@ -221,6 +263,10 @@ def track_storage_write( BlockAccessIndex(tracker.current_block_access_index), value_bytes, ) + # Record in current call frame snapshot if exists + if tracker.call_frame_snapshots: + snapshot = tracker.call_frame_snapshots[-1] + snapshot.storage_writes[(address, key)] = new_value else: add_storage_read(tracker.block_access_list_builder, address, key) @@ -249,13 +295,21 @@ def track_balance_change( """ track_address_access(tracker, address) + block_access_index = BlockAccessIndex(tracker.current_block_access_index) add_balance_change( tracker.block_access_list_builder, address, - BlockAccessIndex(tracker.current_block_access_index), + block_access_index, new_balance, ) + # Record in current call frame snapshot if exists + if tracker.call_frame_snapshots: + snapshot = tracker.call_frame_snapshots[-1] + snapshot.balance_changes.add( + (address, block_access_index, new_balance) + ) + def track_nonce_change( tracker: StateChangeTracker, address: Address, new_nonce: Uint @@ -282,13 +336,20 @@ def track_nonce_change( [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2 """ track_address_access(tracker, address) + block_access_index = BlockAccessIndex(tracker.current_block_access_index) + nonce_u64 = U64(new_nonce) add_nonce_change( tracker.block_access_list_builder, address, - BlockAccessIndex(tracker.current_block_access_index), - U64(new_nonce), + block_access_index, + nonce_u64, ) + # Record in current call frame snapshot if exists + if tracker.call_frame_snapshots: + snapshot = tracker.call_frame_snapshots[-1] + snapshot.nonce_changes.add((address, block_access_index, nonce_u64)) + def track_code_change( tracker: StateChangeTracker, address: Address, new_code: Bytes @@ -313,13 +374,19 @@ def track_code_change( [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2 """ track_address_access(tracker, address) + block_access_index = BlockAccessIndex(tracker.current_block_access_index) add_code_change( tracker.block_access_list_builder, address, - BlockAccessIndex(tracker.current_block_access_index), + block_access_index, new_code, ) + # Record in current call frame snapshot if exists + if tracker.call_frame_snapshots: + snapshot = tracker.call_frame_snapshots[-1] + snapshot.code_changes.add((address, block_access_index, new_code)) + def finalize_transaction_changes( tracker: StateChangeTracker, state: "State" @@ -339,3 +406,120 @@ def finalize_transaction_changes( The current execution state. """ pass + + +def begin_call_frame(tracker: StateChangeTracker) -> None: + """ + Begin a new call frame for tracking reverts. + + Creates a new snapshot to track changes within this call frame. + This allows proper handling of reverts as specified in EIP-7928. + + Parameters + ---------- + tracker : + The state change tracker instance. + """ + tracker.call_frame_snapshots.append(CallFrameSnapshot()) + + +def rollback_call_frame(tracker: StateChangeTracker) -> None: + """ + Rollback changes from the current call frame. + + When a call reverts, this function: + - Converts storage writes to reads + - Removes balance, nonce, and code changes + - Preserves touched addresses + + This implements EIP-7928 revert handling where reverted writes + become reads and addresses remain in the access list. + + Parameters + ---------- + tracker : + The state change tracker instance. + """ + if not tracker.call_frame_snapshots: + return + + snapshot = tracker.call_frame_snapshots.pop() + builder = tracker.block_access_list_builder + + # Convert storage writes to reads + for (address, slot), _ in snapshot.storage_writes.items(): + # Remove the write from storage_changes + if address in builder.accounts: + account_data = builder.accounts[address] + if slot in account_data.storage_changes: + # Filter out changes from this call frame + account_data.storage_changes[slot] = [ + change + for change in account_data.storage_changes[slot] + if change.block_access_index + != tracker.current_block_access_index + ] + if not account_data.storage_changes[slot]: + del account_data.storage_changes[slot] + # Add as a read instead + account_data.storage_reads.add(slot) + + # Remove balance changes from this call frame + for address, block_access_index, new_balance in snapshot.balance_changes: + if address in builder.accounts: + account_data = builder.accounts[address] + # Filter out balance changes from this call frame + account_data.balance_changes = [ + change + for change in account_data.balance_changes + if not ( + change.block_access_index == block_access_index + and change.post_balance == new_balance + ) + ] + + # Remove nonce changes from this call frame + for address, block_access_index, new_nonce in snapshot.nonce_changes: + if address in builder.accounts: + account_data = builder.accounts[address] + # Filter out nonce changes from this call frame + account_data.nonce_changes = [ + change + for change in account_data.nonce_changes + if not ( + change.block_access_index == block_access_index + and change.new_nonce == new_nonce + ) + ] + + # Remove code changes from this call frame + for address, block_access_index, new_code in snapshot.code_changes: + if address in builder.accounts: + account_data = builder.accounts[address] + # Filter out code changes from this call frame + account_data.code_changes = [ + change + for change in account_data.code_changes + if not ( + change.block_access_index == block_access_index + and change.new_code == new_code + ) + ] + + # All touched addresses remain in the access list (already tracked) + + +def commit_call_frame(tracker: StateChangeTracker) -> None: + """ + Commit changes from the current call frame. + + Removes the current call frame snapshot without rolling back changes. + Called when a call completes successfully. + + Parameters + ---------- + tracker : + The state change tracker instance. + """ + if tracker.call_frame_snapshots: + tracker.call_frame_snapshots.pop() diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 569d1c81ec..4ad0cb66d2 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -33,7 +33,7 @@ from .block_access_lists.builder import build_block_access_list from .block_access_lists.rlp_utils import compute_block_access_list_hash from .block_access_lists.tracker import ( - set_transaction_index, + set_block_access_index, track_balance_change, ) from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt @@ -764,9 +764,9 @@ def apply_body( """ block_output = vm.BlockOutput() - # Set system transaction index for pre-execution system contracts + # Set block access index for pre-execution system contracts # EIP-7928: System contracts use block_access_index 0 - set_transaction_index(block_env.state.change_tracker, Uint(0)) + set_block_access_index(block_env.state.change_tracker, Uint(0)) process_unchecked_system_transaction( block_env=block_env, @@ -785,7 +785,9 @@ def apply_body( # EIP-7928: Post-execution uses block_access_index len(transactions) + 1 post_execution_index = ulen(transactions) + Uint(1) - set_transaction_index(block_env.state.change_tracker, post_execution_index) + set_block_access_index( + block_env.state.change_tracker, post_execution_index + ) process_withdrawals(block_env, block_output, withdrawals) @@ -874,7 +876,8 @@ def process_transaction( Index of the transaction in the block. """ # EIP-7928: Transactions use block_access_index 1 to len(transactions) - set_transaction_index(block_env.state.change_tracker, index + Uint(1)) + # Transaction at index i gets block_access_index i+1 + set_block_access_index(block_env.state.change_tracker, index + Uint(1)) trie_set( block_output.transactions_trie, diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index fb893aaa6b..f217c8dafd 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -30,6 +30,12 @@ evm_trace, ) +from ..block_access_lists.tracker import ( + begin_call_frame, + commit_call_frame, + rollback_call_frame, + track_address_access, +) from ..blocks import Log from ..fork_types import Address from ..state import ( @@ -239,6 +245,11 @@ def process_message(message: Message) -> Evm: # take snapshot of state before processing the message begin_transaction(state, transient_storage) + if hasattr(state, 'change_tracker') and state.change_tracker: + begin_call_frame(state.change_tracker) + # Track target address access when processing a message + track_address_access(state.change_tracker, message.current_target) + if message.should_transfer_value and message.value != 0: move_ether( state, message.caller, message.current_target, message.value @@ -249,8 +260,12 @@ def process_message(message: Message) -> Evm: # revert state to the last saved checkpoint # since the message call resulted in an error rollback_transaction(state, transient_storage) + if hasattr(state, 'change_tracker') and state.change_tracker: + rollback_call_frame(state.change_tracker) else: commit_transaction(state, transient_storage) + if hasattr(state, 'change_tracker') and state.change_tracker: + commit_call_frame(state.change_tracker) return evm 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 fac4de4d39..e9469adeeb 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -137,10 +137,10 @@ def compute_block_access_list_hash(self) -> Any: ) @property - def set_transaction_index(self) -> Any: - """set_transaction_index function of the fork""" + def set_block_access_index(self) -> Any: + """set_block_access_index function of the fork""" return ( - self._module("block_access_lists").set_transaction_index + self._module("block_access_lists").set_block_access_index ) @property diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index d5471b5b41..6435a967cd 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -253,7 +253,7 @@ def run_state_test(self) -> Any: def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: if self.fork.is_after_fork("ethereum.forks.amsterdam"): - self.fork.set_transaction_index( + self.fork.set_block_access_index( block_env.state.change_tracker, Uint(0) ) if self.fork.is_after_fork("ethereum.forks.prague"): @@ -295,7 +295,7 @@ def _run_blockchain_test(self, block_env: Any, block_output: Any) -> None: # post-execution use n + 1 post_execution_index = num_transactions + Uint(1) - self.fork.set_transaction_index( + self.fork.set_block_access_index( block_env.state.change_tracker, post_execution_index ) diff --git a/tests/amsterdam/test_bal_implementation.py b/tests/amsterdam/test_bal_implementation.py index 988c11dcc6..5320d7ec99 100644 --- a/tests/amsterdam/test_bal_implementation.py +++ b/tests/amsterdam/test_bal_implementation.py @@ -8,8 +8,6 @@ - Edge cases and error handling """ -from unittest.mock import MagicMock, patch - import pytest from ethereum_types.bytes import Bytes, Bytes20, Bytes32 from ethereum_types.numeric import U64, U256, Uint @@ -27,7 +25,7 @@ ) from ethereum.forks.amsterdam.block_access_lists.tracker import ( capture_pre_state, - set_transaction_index, + set_block_access_index, track_balance_change, track_code_change, track_nonce_change, @@ -194,14 +192,14 @@ def test_tracker_initialization(self) -> None: assert tracker.pre_storage_cache == {} assert tracker.current_block_access_index == 0 - def test_tracker_set_transaction_index(self) -> None: + def test_tracker_set_block_access_index(self) -> None: """Test setting block access index.""" builder = BlockAccessListBuilder() tracker = StateChangeTracker(builder) - set_transaction_index(tracker, 5) + set_block_access_index(tracker, 5) assert tracker.current_block_access_index == 5 - # Pre-storage cache should persist across transactions + # Pre-storage cache should be cleared for new block access index assert tracker.pre_storage_cache == {} @patch("ethereum.forks.amsterdam.state.get_storage") @@ -612,5 +610,229 @@ def test_address_sorting(self) -> None: assert account.address == sorted_addresses[i] +class TestValueCalls: + """Test value call scenarios including 0 ETH calls.""" + + def test_zero_eth_value_call_tracks_address_without_balance(self) -> None: + """Test that 0 ETH calls track recipient address without balance changes.""" + from ethereum.forks.amsterdam.block_access_lists.tracker import track_address_access + + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + set_block_access_index(tracker, Uint(1)) + + recipient = Bytes20(b"\x02" * 20) + + # Track only the address access without balance change + track_address_access(tracker, recipient) + + block_access_list = build_block_access_list(builder) + + # Verify recipient is tracked without balance changes + recipient_found = False + for account in block_access_list.account_changes: + if account.address == recipient: + recipient_found = True + assert len(account.balance_changes) == 0 + break + + assert recipient_found + + def test_nonzero_eth_value_call_tracks_with_balance(self) -> None: + """Test that non-zero ETH calls track addresses with balance changes.""" + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + set_block_access_index(tracker, Uint(1)) + + sender = Bytes20(b"\x01" * 20) + recipient = Bytes20(b"\x02" * 20) + + # Track balance changes for value transfer + track_balance_change(tracker, sender, U256(900)) + track_balance_change(tracker, recipient, U256(100)) + + block_access_list = build_block_access_list(builder) + + # Verify both addresses tracked with balance changes + sender_found = False + recipient_found = False + + for account in block_access_list.account_changes: + if account.address == sender: + sender_found = True + assert len(account.balance_changes) == 1 + assert account.balance_changes[0].post_balance == U256(900) + elif account.address == recipient: + recipient_found = True + assert len(account.balance_changes) == 1 + assert account.balance_changes[0].post_balance == U256(100) + + assert sender_found and recipient_found + + def test_multiple_zero_eth_calls_deduplication(self) -> None: + """Test that multiple 0 ETH calls to same address are deduplicated.""" + from ethereum.forks.amsterdam.block_access_lists.tracker import track_address_access + + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + set_block_access_index(tracker, Uint(1)) + + recipient = Bytes20(b"\x02" * 20) + + # Multiple calls to same address + track_address_access(tracker, recipient) + track_address_access(tracker, recipient) + track_address_access(tracker, recipient) + + block_access_list = build_block_access_list(builder) + + # Verify address appears exactly once without balance changes + recipient_count = sum(1 for account in block_access_list.account_changes + if account.address == recipient) + assert recipient_count == 1 + + for account in block_access_list.account_changes: + if account.address == recipient: + assert len(account.balance_changes) == 0 + + +class TestRevertScenarios: + """Test block access list behavior during reverts.""" + + def test_storage_write_becomes_read_on_revert(self) -> None: + """Test that storage writes become reads when transaction reverts.""" + from ethereum.forks.amsterdam.block_access_lists.tracker import ( + begin_call_frame, + rollback_call_frame, + track_storage_write, + track_storage_read, + ) + + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + set_block_access_index(tracker, Uint(1)) + + address = Bytes20(b"\x01" * 20) + slot1 = Bytes32(b"\x01" * 32) + slot2 = Bytes32(b"\x02" * 32) + + # Begin call frame + begin_call_frame(tracker) + + # Mock state for storage operations + class MockState: + pass + state = MockState() + + # Track storage operations that will be reverted + track_storage_read(tracker, address, slot1, state) # Read slot 0x01 + + # Storage write to slot 0x02 (will be reverted) + track_storage_write(tracker, address, slot2, U256(42), state) + + # Rollback the call frame (simulating revert) + rollback_call_frame(tracker) + + # Build and check the access list + block_access_list = build_block_access_list(builder) + + # Find the account in the access list + account_found = False + for account in block_access_list.account_changes: + if account.address == address: + account_found = True + # Both slots should be in storage_reads + assert slot1 in builder.accounts[address].storage_reads + assert slot2 in builder.accounts[address].storage_reads + # No storage changes should exist + assert len(account.storage_changes) == 0 + break + + assert account_found + + def test_balance_changes_removed_on_revert(self) -> None: + """Test that balance changes are removed on revert but address remains.""" + from ethereum.forks.amsterdam.block_access_lists.tracker import ( + begin_call_frame, + rollback_call_frame, + ) + + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + set_block_access_index(tracker, Uint(1)) + + address = Bytes20(b"\x01" * 20) + + # Begin call frame + begin_call_frame(tracker) + + # Track balance change that will be reverted + track_balance_change(tracker, address, U256(1000)) + + # Rollback the call frame + rollback_call_frame(tracker) + + # Build and check the access list + block_access_list = build_block_access_list(builder) + + # Address should still be in access list but without balance changes + account_found = False + for account in block_access_list.account_changes: + if account.address == address: + account_found = True + assert len(account.balance_changes) == 0 + break + + assert account_found + + def test_nested_call_frames_with_partial_revert(self) -> None: + """Test nested call frames where inner frame reverts but outer succeeds.""" + from ethereum.forks.amsterdam.block_access_lists.tracker import ( + begin_call_frame, + commit_call_frame, + rollback_call_frame, + ) + + builder = BlockAccessListBuilder() + tracker = StateChangeTracker(builder) + set_block_access_index(tracker, Uint(1)) + + address1 = Bytes20(b"\x01" * 20) + address2 = Bytes20(b"\x02" * 20) + + # Outer call frame + begin_call_frame(tracker) + track_balance_change(tracker, address1, U256(900)) + + # Inner call frame (will be reverted) + begin_call_frame(tracker) + track_balance_change(tracker, address2, U256(100)) + + # Rollback inner frame + rollback_call_frame(tracker) + + # Commit outer frame + commit_call_frame(tracker) + + # Build and check the access list + block_access_list = build_block_access_list(builder) + + # address1 should have balance change, address2 should not + address1_found = False + address2_found = False + + for account in block_access_list.account_changes: + if account.address == address1: + address1_found = True + assert len(account.balance_changes) == 1 + assert account.balance_changes[0].post_balance == U256(900) + elif account.address == address2: + address2_found = True + assert len(account.balance_changes) == 0 + + assert address1_found + assert address2_found # Address2 touched but no changes + + if __name__ == "__main__": pytest.main([__file__, "-v"]) From 7369041af27c13135cdfb4bbed18c4cc60e9d474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= Date: Mon, 22 Sep 2025 10:42:18 +0200 Subject: [PATCH 09/18] fix call/delagate call tracking bug --- src/ethereum/forks/amsterdam/vm/instructions/system.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index 5d0d0c9a5c..075472a227 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -494,6 +494,10 @@ def callcode(evm: Evm) -> None: ) charge_gas(evm, message_call_gas.cost + extend_memory.cost) + track_address_access( + evm.message.block_env.state.change_tracker, code_address + ) + # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( @@ -632,6 +636,10 @@ def delegatecall(evm: Evm) -> None: ) charge_gas(evm, message_call_gas.cost + extend_memory.cost) + track_address_access( + evm.message.block_env.state.change_tracker, code_address + ) + # OPERATION evm.memory += b"\x00" * extend_memory.expand_by generic_call( From 8c82882175db17a81a3ee86e1827df3c51480337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= Date: Tue, 23 Sep 2025 15:45:16 +0200 Subject: [PATCH 10/18] fix self destruct in same transaction bug --- src/ethereum/forks/amsterdam/fork.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 4ad0cb66d2..e9577a1201 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -35,6 +35,8 @@ from .block_access_lists.tracker import ( set_block_access_index, track_balance_change, + track_code_change, + track_nonce_change, ) from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt from .bloom import logs_bloom @@ -996,6 +998,9 @@ def process_transaction( destroy_account(block_env.state, block_env.coinbase) for address in tx_output.accounts_to_delete: + # Track final state before destruction + track_code_change(block_env.state.change_tracker, address, Bytes()) + track_nonce_change(block_env.state.change_tracker, address, Uint(0)) destroy_account(block_env.state, address) block_output.block_gas_used += tx_gas_used_after_refund From 08b46ef83300da7a47f9ee18884620e1700259e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= Date: Wed, 24 Sep 2025 08:09:26 +0200 Subject: [PATCH 11/18] fix duplicated code entries for in transaction self destruct --- .../forks/amsterdam/block_access_lists/builder.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/ethereum/forks/amsterdam/block_access_lists/builder.py b/src/ethereum/forks/amsterdam/block_access_lists/builder.py index 113d868563..2e8ff588a2 100644 --- a/src/ethereum/forks/amsterdam/block_access_lists/builder.py +++ b/src/ethereum/forks/amsterdam/block_access_lists/builder.py @@ -308,6 +308,20 @@ def add_code_change( """ 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 ) From 6abe0ecb792265211d93bc3fea2f932e5d2cfa90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= <51536394+nerolation@users.noreply.github.com> Date: Mon, 29 Sep 2025 01:40:24 +0200 Subject: [PATCH 12/18] Fix self-destruct cases with pre-execution balance cache / tracking * fix self-destruct implementation * fix self-destruct tracking balance * fix it in the bal finalization by filtering * add balance reset and fix tests * simplify pre-balance tracking not using snapshots * fix: lint issues --------- Co-authored-by: fselmo --- .../amsterdam/block_access_lists/tracker.py | 136 +++++++++++++++++- src/ethereum/forks/amsterdam/fork.py | 20 ++- src/ethereum/forks/amsterdam/state.py | 7 + tests/amsterdam/test_bal_implementation.py | 13 +- 4 files changed, 165 insertions(+), 11 deletions(-) diff --git a/src/ethereum/forks/amsterdam/block_access_lists/tracker.py b/src/ethereum/forks/amsterdam/block_access_lists/tracker.py index 1ad068a604..f10d4aa1ac 100644 --- a/src/ethereum/forks/amsterdam/block_access_lists/tracker.py +++ b/src/ethereum/forks/amsterdam/block_access_lists/tracker.py @@ -97,6 +97,14 @@ class StateChangeTracker: from the beginning of the current transaction. """ + pre_balance_cache: Dict[Address, U256] = field(default_factory=dict) + """ + Cache of pre-transaction balance values, keyed by address. + This cache is cleared at the start of each transaction and used by + finalize_transaction_changes to filter out balance changes where + the final balance equals the initial balance. + """ + current_block_access_index: Uint = Uint(0) """ The current block access index (0 for pre-execution, @@ -135,6 +143,8 @@ def set_block_access_index( # Clear the pre-storage cache for each new transaction to ensure # no-op writes are detected relative to the transaction start tracker.pre_storage_cache.clear() + # Clear the pre-balance cache for each new transaction + tracker.pre_balance_cache.clear() def capture_pre_state( @@ -271,6 +281,45 @@ def track_storage_write( add_storage_read(tracker.block_access_list_builder, address, key) +def capture_pre_balance( + tracker: StateChangeTracker, address: Address, state: "State" +) -> U256: + """ + Capture and cache the pre-transaction balance for an account. + + This function caches the balance on first access for each address during + a transaction. It must be called before any balance modifications are made + to ensure we capture the pre-transaction balance correctly. The cache is + cleared at the beginning of each transaction. + + This is used by finalize_transaction_changes to determine which balance + changes should be filtered out. + + Parameters + ---------- + tracker : + The state change tracker instance. + address : + The account address. + state : + The current execution state. + + Returns + ------- + value : + The balance at the beginning of the current transaction. + """ + if address not in tracker.pre_balance_cache: + # Import locally to avoid circular import + from ..state import get_account + + # Cache the current balance on first access + # This should be called before any balance modifications + account = get_account(state, address) + tracker.pre_balance_cache[address] = account.balance + return tracker.pre_balance_cache[address] + + def track_balance_change( tracker: StateChangeTracker, address: Address, @@ -388,15 +437,71 @@ def track_code_change( snapshot.code_changes.add((address, block_access_index, new_code)) +def handle_in_transaction_selfdestruct( + tracker: StateChangeTracker, address: Address +) -> None: + """ + Handle an account that self-destructed in the same transaction it was + created. + + Per EIP-7928, accounts destroyed within their creation transaction must be + included as read-only with storage writes converted to reads. Nonce and + code changes from the current transaction are also removed. + + Note: Balance changes are handled separately by + finalize_transaction_changes. + + Parameters + ---------- + tracker : + The state change tracker instance. + address : + The address that self-destructed. + """ + builder = tracker.block_access_list_builder + if address not in builder.accounts: + return + + account_data = builder.accounts[address] + current_index = tracker.current_block_access_index + + # Convert storage writes from current tx to reads + for slot in list(account_data.storage_changes.keys()): + account_data.storage_changes[slot] = [ + c for c in account_data.storage_changes[slot] + if c.block_access_index != current_index + ] + if not account_data.storage_changes[slot]: + del account_data.storage_changes[slot] + account_data.storage_reads.add(slot) + + # Remove nonce and code changes from current transaction + account_data.nonce_changes = [ + c for c in account_data.nonce_changes + if c.block_access_index != current_index + ] + account_data.code_changes = [ + c for c in account_data.code_changes + if c.block_access_index != current_index + ] + + def finalize_transaction_changes( tracker: StateChangeTracker, state: "State" ) -> None: """ Finalize changes for the current transaction. - This method is called at the end of each transaction execution. Currently, - a no-op as all tracking is done incrementally during execution, but - provided for future extensibility. + This method is called at the end of each transaction execution to filter + out spurious balance changes. It removes all balance changes for addresses + where the post-transaction balance equals the pre-transaction balance. + + This is crucial for handling cases like: + - In-transaction self-destructs where an account with 0 balance is created + and destroyed, resulting in no net balance change + - Round-trip transfers where an account receives and sends equal amounts + + Only actual state changes are recorded in the Block Access List. Parameters ---------- @@ -405,7 +510,30 @@ def finalize_transaction_changes( state : The current execution state. """ - pass + # Import locally to avoid circular import + from ..state import get_account + + builder = tracker.block_access_list_builder + current_index = tracker.current_block_access_index + + # Check each address that had balance changes in this transaction + for address in list(builder.accounts.keys()): + account_data = builder.accounts[address] + + # Get the pre-transaction balance + pre_balance = capture_pre_balance(tracker, address, state) + + # Get the current (post-transaction) balance + post_balance = get_account(state, address).balance + + # If pre-tx balance equals post-tx balance, remove all balance changes + # for this address in the current transaction + if pre_balance == post_balance: + # Filter out balance changes from the current transaction + account_data.balance_changes = [ + change for change in account_data.balance_changes + if change.block_access_index != current_index + ] def begin_call_frame(tracker: StateChangeTracker) -> None: diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index e9577a1201..3c16176686 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -33,10 +33,10 @@ from .block_access_lists.builder import build_block_access_list from .block_access_lists.rlp_utils import compute_block_access_list_hash from .block_access_lists.tracker import ( + finalize_transaction_changes, + handle_in_transaction_selfdestruct, set_block_access_index, track_balance_change, - track_code_change, - track_nonce_change, ) from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt from .bloom import logs_bloom @@ -998,11 +998,21 @@ def process_transaction( destroy_account(block_env.state, block_env.coinbase) for address in tx_output.accounts_to_delete: - # Track final state before destruction - track_code_change(block_env.state.change_tracker, address, Bytes()) - track_nonce_change(block_env.state.change_tracker, address, Uint(0)) + # EIP-7928: In-transaction self-destruct - convert storage writes to + # reads and remove nonce/code changes. Only accounts created in same + # tx are in accounts_to_delete per EIP-6780. + handle_in_transaction_selfdestruct( + block_env.state.change_tracker, address + ) destroy_account(block_env.state, address) + # EIP-7928: Finalize transaction changes + # Remove balance changes where post-tx balance equals pre-tx balance + finalize_transaction_changes( + block_env.state.change_tracker, + block_env.state, + ) + block_output.block_gas_used += tx_gas_used_after_refund block_output.blob_gas_used += tx_blob_gas_used diff --git a/src/ethereum/forks/amsterdam/state.py b/src/ethereum/forks/amsterdam/state.py index 06995554c6..e916dbb4c9 100644 --- a/src/ethereum/forks/amsterdam/state.py +++ b/src/ethereum/forks/amsterdam/state.py @@ -502,6 +502,10 @@ def move_ether( """ Move funds between accounts. """ + # Capture pre-transaction balances before first modification + from .block_access_lists.tracker import capture_pre_balance + capture_pre_balance(state.change_tracker, sender_address, state) + capture_pre_balance(state.change_tracker, recipient_address, state) def reduce_sender_balance(sender: Account) -> None: if sender.balance < amount: @@ -540,6 +544,9 @@ def set_account_balance(state: State, address: Address, amount: U256) -> None: amount: The amount that needs to set in balance. """ + # Capture pre-transaction balance before first modification + from .block_access_lists.tracker import capture_pre_balance + capture_pre_balance(state.change_tracker, address, state) def set_balance(account: Account) -> None: account.balance = amount diff --git a/tests/amsterdam/test_bal_implementation.py b/tests/amsterdam/test_bal_implementation.py index 5320d7ec99..2c8ba51c4d 100644 --- a/tests/amsterdam/test_bal_implementation.py +++ b/tests/amsterdam/test_bal_implementation.py @@ -9,6 +9,7 @@ """ import pytest +from unittest.mock import MagicMock, patch from ethereum_types.bytes import Bytes, Bytes20, Bytes32 from ethereum_types.numeric import U64, U256, Uint @@ -699,7 +700,8 @@ def test_multiple_zero_eth_calls_deduplication(self) -> None: class TestRevertScenarios: """Test block access list behavior during reverts.""" - def test_storage_write_becomes_read_on_revert(self) -> None: + @patch("ethereum.forks.amsterdam.state.get_storage") + def test_storage_write_becomes_read_on_revert(self, mock_get_storage: MagicMock) -> None: """Test that storage writes become reads when transaction reverts.""" from ethereum.forks.amsterdam.block_access_lists.tracker import ( begin_call_frame, @@ -721,12 +723,19 @@ def test_storage_write_becomes_read_on_revert(self) -> None: # Mock state for storage operations class MockState: - pass + _storage_tries = {} + state = MockState() + # Mock get_storage to return U256(0) for reads + mock_get_storage.return_value = U256(0) + # Track storage operations that will be reverted track_storage_read(tracker, address, slot1, state) # Read slot 0x01 + # Mock get_storage to return old value for slot2 + mock_get_storage.return_value = U256(10) # Different from new value + # Storage write to slot 0x02 (will be reverted) track_storage_write(tracker, address, slot2, U256(42), state) From 5d98d409da10f763754dea8d60e855395f944498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= <51536394+nerolation@users.noreply.github.com> Date: Wed, 8 Oct 2025 22:45:21 +0200 Subject: [PATCH 13/18] fix non-tracked 7702 authority for invalid delegations (#16) * fix non-tracked 7702 authority for invalid delegations * fix: lint issues * fix: track delegation target when loaded as call target * fix: track delegation target when loaded as call target from call opcodes * chore: fix issues with documentation generation --------- Co-authored-by: fselmo --- src/ethereum/forks/amsterdam/vm/eoa_delegation.py | 13 +++++++++++++ .../forks/amsterdam/vm/instructions/system.py | 8 -------- src/ethereum/forks/amsterdam/vm/interpreter.py | 6 ++++++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py index 1fe2e1e7bd..90a8bb13a5 100644 --- a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -13,6 +13,7 @@ from ethereum.crypto.hash import keccak256 from ethereum.exceptions import InvalidBlock, InvalidSignatureError +from ..block_access_lists.tracker import track_address_access from ..fork_types import Address, Authorization from ..state import account_exists, get_account, increment_nonce, set_code from ..utils.hexadecimal import hex_to_address @@ -131,6 +132,10 @@ def access_delegation( The delegation address, code, and access gas cost. """ state = evm.message.block_env.state + + # EIP-7928: Track the authority address (delegated account being called) + track_address_access(state.change_tracker, address) + code = get_account(state, address).code if not is_valid_delegation(code): return False, address, code, Uint(0) @@ -143,6 +148,10 @@ def access_delegation( access_gas_cost = GAS_COLD_ACCOUNT_ACCESS code = get_account(state, address).code + # EIP-7928: Track delegation target when loaded as call target + # `address` here is now the delegation target account + track_address_access(state.change_tracker, address) + return True, address, code, access_gas_cost @@ -181,6 +190,10 @@ def set_delegation(message: Message) -> U256: authority_account = get_account(state, authority) authority_code = authority_account.code + # EIP-7928: Track authority account access in BAL even if delegation + # fails + track_address_access(state.change_tracker, authority) + if authority_code and not is_valid_delegation(authority_code): continue diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index 075472a227..5d0d0c9a5c 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -494,10 +494,6 @@ def callcode(evm: Evm) -> None: ) charge_gas(evm, message_call_gas.cost + extend_memory.cost) - track_address_access( - evm.message.block_env.state.change_tracker, code_address - ) - # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( @@ -636,10 +632,6 @@ def delegatecall(evm: Evm) -> None: ) charge_gas(evm, message_call_gas.cost + extend_memory.cost) - track_address_access( - evm.message.block_env.state.change_tracker, code_address - ) - # OPERATION evm.memory += b"\x00" * extend_memory.expand_by generic_call( diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index f217c8dafd..b16b754965 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -139,6 +139,12 @@ def process_message_call(message: Message) -> MessageCallOutput: message.code = get_account(block_env.state, delegated_address).code message.code_address = delegated_address + # EIP-7928: Track delegation target when loaded as call target + track_address_access( + block_env.state.change_tracker, + delegated_address, + ) + evm = process_message(message) if evm.error: From f3ad59980a68fe5974244f41bcda7b22294bf98b Mon Sep 17 00:00:00 2001 From: felipe Date: Mon, 13 Oct 2025 08:04:41 -0600 Subject: [PATCH 14/18] refactor: Put back explicit acct tracking outside of 7702 delegation path (#17) --- src/ethereum/forks/amsterdam/vm/eoa_delegation.py | 7 +++---- src/ethereum/forks/amsterdam/vm/instructions/system.py | 8 ++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py index 90a8bb13a5..8590eea835 100644 --- a/src/ethereum/forks/amsterdam/vm/eoa_delegation.py +++ b/src/ethereum/forks/amsterdam/vm/eoa_delegation.py @@ -133,13 +133,13 @@ def access_delegation( """ state = evm.message.block_env.state - # EIP-7928: Track the authority address (delegated account being called) - track_address_access(state.change_tracker, address) - code = get_account(state, address).code if not is_valid_delegation(code): return False, address, code, Uint(0) + # EIP-7928: Track the authority address (delegated account being called) + track_address_access(state.change_tracker, address) + address = Address(code[EOA_DELEGATION_MARKER_LENGTH:]) if address in evm.accessed_addresses: access_gas_cost = GAS_WARM_ACCESS @@ -149,7 +149,6 @@ def access_delegation( code = get_account(state, address).code # EIP-7928: Track delegation target when loaded as call target - # `address` here is now the delegation target account track_address_access(state.change_tracker, address) return True, address, code, access_gas_cost diff --git a/src/ethereum/forks/amsterdam/vm/instructions/system.py b/src/ethereum/forks/amsterdam/vm/instructions/system.py index 5d0d0c9a5c..075472a227 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/system.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/system.py @@ -494,6 +494,10 @@ def callcode(evm: Evm) -> None: ) charge_gas(evm, message_call_gas.cost + extend_memory.cost) + track_address_access( + evm.message.block_env.state.change_tracker, code_address + ) + # OPERATION evm.memory += b"\x00" * extend_memory.expand_by sender_balance = get_account( @@ -632,6 +636,10 @@ def delegatecall(evm: Evm) -> None: ) charge_gas(evm, message_call_gas.cost + extend_memory.cost) + track_address_access( + evm.message.block_env.state.change_tracker, code_address + ) + # OPERATION evm.memory += b"\x00" * extend_memory.expand_by generic_call( From 3496e719b515bc82f35c42f83e78d426d31283ba Mon Sep 17 00:00:00 2001 From: felipe Date: Tue, 14 Oct 2025 07:57:11 -0600 Subject: [PATCH 15/18] fix: track implicit SLOAD within SSTORE for OOG cases (#18) --- src/ethereum/forks/amsterdam/vm/instructions/storage.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ethereum/forks/amsterdam/vm/instructions/storage.py b/src/ethereum/forks/amsterdam/vm/instructions/storage.py index ab5da8376f..202c795cb2 100644 --- a/src/ethereum/forks/amsterdam/vm/instructions/storage.py +++ b/src/ethereum/forks/amsterdam/vm/instructions/storage.py @@ -100,6 +100,15 @@ def sstore(evm: Evm) -> None: ) current_value = get_storage(state, evm.message.current_target, key) + # Track the implicit SLOAD that occurs in SSTORE + # This must happen BEFORE charge_gas() so reads are recorded even if OOG + track_storage_read( + state.change_tracker, + evm.message.current_target, + key, + evm.message.block_env.state, + ) + gas_cost = Uint(0) if (evm.message.current_target, key) not in evm.accessed_storage_keys: From c53b609262a7309d8bdbd44057e6eb87292b27c0 Mon Sep 17 00:00:00 2001 From: felipe Date: Wed, 15 Oct 2025 12:18:58 -0600 Subject: [PATCH 16/18] fix: do not track setting empty code to a new account (#19) --- src/ethereum/forks/amsterdam/state.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ethereum/forks/amsterdam/state.py b/src/ethereum/forks/amsterdam/state.py index e916dbb4c9..a3bff461b7 100644 --- a/src/ethereum/forks/amsterdam/state.py +++ b/src/ethereum/forks/amsterdam/state.py @@ -604,7 +604,12 @@ def write_code(sender: Account) -> None: modify_state(state, address, write_code) - track_code_change(state.change_tracker, address, code) + # Only track code changes if it's not setting empty code on a + # newly created address. For newly created addresses, setting + # code to b"" is not a meaningful state change since the address + # had no code to begin with. + if not (code == b"" and address in state.created_accounts): + track_code_change(state.change_tracker, address, code) def get_storage_original(state: State, address: Address, key: Bytes32) -> U256: From 935e7b8bd2a342a0f2c07722dc40a92cb065fd1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= Date: Thu, 23 Oct 2025 14:53:51 +0200 Subject: [PATCH 17/18] fix coinbase tracking as discussed in breakout call nr 5 --- src/ethereum/forks/amsterdam/fork.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 3c16176686..3c982cde99 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -36,6 +36,7 @@ finalize_transaction_changes, handle_in_transaction_selfdestruct, set_block_access_index, + track_address_access, track_balance_change, ) from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt @@ -985,6 +986,10 @@ def process_transaction( set_account_balance(block_env.state, sender, sender_balance_after_refund) # transfer miner fees + # EIP-7928: Always track coinbase access, even for zero rewards + # This ensures coinbase appears in BAL as read-only for zero-value rewards + track_address_access(block_env.state.change_tracker, block_env.coinbase) + coinbase_balance_after_mining_fee = get_account( block_env.state, block_env.coinbase ).balance + U256(transaction_fee) From f3384a2489ab028ecbcfe2a050db459d59a28559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= Date: Thu, 23 Oct 2025 15:06:20 +0200 Subject: [PATCH 18/18] fix linter --- src/ethereum/forks/amsterdam/fork.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index 3c982cde99..8c0d45abc1 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -989,7 +989,7 @@ def process_transaction( # EIP-7928: Always track coinbase access, even for zero rewards # This ensures coinbase appears in BAL as read-only for zero-value rewards track_address_access(block_env.state.change_tracker, block_env.coinbase) - + coinbase_balance_after_mining_fee = get_account( block_env.state, block_env.coinbase ).balance + U256(transaction_fee)