diff --git a/setup.cfg b/setup.cfg index 51da0c34a23..0f98dc5a168 100644 --- a/setup.cfg +++ b/setup.cfg @@ -112,6 +112,7 @@ install_requires = pycryptodome>=3,<4 coincurve>=18,<19 typing_extensions>=4 + eth2spec @ git+https://github.com/ethereum/consensus-specs.git@fe8db03f45609e9dd0abeede10294d77ef6fb92c [options.package_data] ethereum = diff --git a/src/ethereum/base_types.py b/src/ethereum/base_types.py index 1cdc7ba6603..a4a7b746bae 100644 --- a/src/ethereum/base_types.py +++ b/src/ethereum/base_types.py @@ -956,6 +956,14 @@ class Bytes32(FixedBytes): LENGTH = 32 +class Bytes48(FixedBytes): + """ + Byte array of exactly 48 elements. + """ + + LENGTH = 48 + + class Bytes64(FixedBytes): """ Byte array of exactly 64 elements. diff --git a/src/ethereum/cancun/fork.py b/src/ethereum/cancun/fork.py index 80a04aed7b5..bbb93f9ee53 100644 --- a/src/ethereum/cancun/fork.py +++ b/src/ethereum/cancun/fork.py @@ -34,6 +34,7 @@ TX_DATA_COST_PER_ZERO, AccessListTransaction, Address, + BlobTransaction, Block, Bloom, FeeMarketTransaction, @@ -43,6 +44,7 @@ Receipt, Root, Transaction, + VersionedHash, Withdrawal, decode_transaction, encode_transaction, @@ -61,7 +63,13 @@ from .utils.hexadecimal import hex_to_address from .utils.message import prepare_message from .vm import Message -from .vm.gas import init_code_cost +from .vm.gas import ( + calculate_blob_gas_price, + calculate_data_fee, + calculate_excess_blob_gas, + calculate_total_blob_gas, + init_code_cost, +) from .vm.interpreter import MAX_CODE_SIZE, process_message_call BASE_FEE_MAX_CHANGE_DENOMINATOR = 8 @@ -74,6 +82,8 @@ "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02" ) SYSTEM_TRANSACTION_GAS = Uint(30000000) +MAX_BLOB_GAS_PER_BLOCK = 786432 +VERSIONED_HASH_VERSION_KZG = b"\x01" @dataclass @@ -172,6 +182,9 @@ def state_transition(chain: BlockChain, block: Block) -> None: Block to apply to `chain`. """ parent_header = chain.blocks[-1].header + excess_blob_gas = calculate_excess_blob_gas(parent_header) + ensure(block.header.excess_blob_gas == excess_blob_gas, InvalidBlock) + validate_header(block.header, parent_header) ensure(block.ommers == (), InvalidBlock) ( @@ -181,6 +194,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: block_logs_bloom, state, withdrawals_root, + blob_gas_used, ) = apply_body( chain.state, get_last_256_block_hashes(chain), @@ -194,6 +208,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: chain.chain_id, block.withdrawals, block.header.parent_beacon_block_root, + excess_blob_gas, ) ensure(gas_used == block.header.gas_used, InvalidBlock) ensure(transactions_root == block.header.transactions_root, InvalidBlock) @@ -201,6 +216,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: ensure(receipt_root == block.header.receipt_root, InvalidBlock) ensure(block_logs_bloom == block.header.bloom, InvalidBlock) ensure(withdrawals_root == block.header.withdrawals_root, InvalidBlock) + ensure(blob_gas_used == block.header.blob_gas_used, InvalidBlock) chain.blocks.append(block) if len(chain.blocks) > 255: @@ -350,7 +366,7 @@ def check_transaction( ensure(tx.gas <= gas_available, InvalidBlock) sender_address = recover_sender(chain_id, tx) - if isinstance(tx, FeeMarketTransaction): + if isinstance(tx, (FeeMarketTransaction, BlobTransaction)): ensure(tx.max_fee_per_gas >= tx.max_priority_fee_per_gas, InvalidBlock) ensure(tx.max_fee_per_gas >= base_fee_per_gas, InvalidBlock) @@ -401,8 +417,10 @@ def make_receipt( if isinstance(tx, AccessListTransaction): return b"\x01" + rlp.encode(receipt) - if isinstance(tx, FeeMarketTransaction): + elif isinstance(tx, FeeMarketTransaction): return b"\x02" + rlp.encode(receipt) + elif isinstance(tx, BlobTransaction): + return b"\x03" + rlp.encode(receipt) else: return receipt @@ -420,7 +438,8 @@ def apply_body( chain_id: U64, withdrawals: Tuple[Withdrawal, ...], parent_beacon_block_root: Root, -) -> Tuple[Uint, Root, Root, Bloom, State, Root]: + excess_blob_gas: U64, +) -> Tuple[Uint, Root, Root, Bloom, State, Root, Uint]: """ Executes a block. @@ -461,6 +480,8 @@ def apply_body( Withdrawals to be processed in the current block. parent_beacon_block_root : The root of the beacon block from the parent block. + excess_blob_gas : + Excess blob gas calculated from the previous block. Returns ------- @@ -476,6 +497,7 @@ def apply_body( state : `ethereum.fork_types.State` State after all transactions have been executed. """ + blob_gas_used = Uint(0) gas_available = block_gas_limit transactions_trie: Trie[ Bytes, Optional[Union[Bytes, LegacyTransaction]] @@ -507,6 +529,7 @@ def apply_body( accessed_addresses=set(), accessed_storage_keys=set(), parent_evm=None, + blob_versioned_hashes=(), ) system_tx_env = vm.Environment( @@ -523,6 +546,7 @@ def apply_body( state=state, chain_id=chain_id, traces=[], + excess_blob_gas=excess_blob_gas, ) process_message_call(system_tx_message, system_tx_env) @@ -550,6 +574,7 @@ def apply_body( state=state, chain_id=chain_id, traces=[], + excess_blob_gas=excess_blob_gas, ) gas_used, logs, error = process_transaction(env, tx) @@ -566,7 +591,9 @@ def apply_body( ) block_logs += logs + blob_gas_used += calculate_total_blob_gas(tx) + ensure(blob_gas_used <= MAX_BLOB_GAS_PER_BLOCK, InvalidBlock) block_gas_used = block_gas_limit - gas_available block_logs_bloom = logs_bloom(block_logs) @@ -586,6 +613,7 @@ def apply_body( block_logs_bloom, state, root(withdrawals_trie), + blob_gas_used, ) @@ -623,13 +651,31 @@ def process_transaction( sender = env.origin sender_account = get_account(env.state, sender) - if isinstance(tx, FeeMarketTransaction): - gas_fee = tx.gas * tx.max_fee_per_gas + if isinstance(tx, (FeeMarketTransaction, BlobTransaction)): + max_gas_fee = tx.gas * tx.max_fee_per_gas else: - gas_fee = tx.gas * tx.gas_price + max_gas_fee = tx.gas * tx.gas_price + + if isinstance(tx, BlobTransaction): + ensure(len(tx.blob_versioned_hashes) > 0, InvalidBlock) + for blob_versioned_hash in tx.blob_versioned_hashes: + ensure( + blob_versioned_hash[0:1] == VERSIONED_HASH_VERSION_KZG, + InvalidBlock, + ) + + ensure( + tx.max_fee_per_blob_gas >= calculate_blob_gas_price(env), + InvalidBlock, + ) + + max_gas_fee += calculate_total_blob_gas(tx) * tx.max_fee_per_blob_gas + blob_gas_fee = calculate_data_fee(env, tx) + else: + blob_gas_fee = Uint(0) ensure(sender_account.nonce == tx.nonce, InvalidBlock) - ensure(sender_account.balance >= gas_fee + tx.value, InvalidBlock) + ensure(sender_account.balance >= max_gas_fee + tx.value, InvalidBlock) ensure(sender_account.code == bytearray(), InvalidBlock) effective_gas_fee = tx.gas * env.gas_price @@ -637,18 +683,26 @@ def process_transaction( gas = tx.gas - calculate_intrinsic_cost(tx) increment_nonce(env.state, sender) - sender_balance_after_gas_fee = sender_account.balance - effective_gas_fee + sender_balance_after_gas_fee = ( + sender_account.balance - effective_gas_fee - blob_gas_fee + ) set_account_balance(env.state, sender, sender_balance_after_gas_fee) preaccessed_addresses = set() preaccessed_storage_keys = set() preaccessed_addresses.add(env.coinbase) - if isinstance(tx, (AccessListTransaction, FeeMarketTransaction)): + if isinstance( + tx, (AccessListTransaction, FeeMarketTransaction, BlobTransaction) + ): for address, keys in tx.access_list: preaccessed_addresses.add(address) for key in keys: preaccessed_storage_keys.add((address, key)) + blob_versioned_hashes: Tuple[VersionedHash, ...] = () + if isinstance(tx, BlobTransaction): + blob_versioned_hashes = tx.blob_versioned_hashes + message = prepare_message( sender, tx.to, @@ -658,6 +712,7 @@ def process_transaction( env, preaccessed_addresses=frozenset(preaccessed_addresses), preaccessed_storage_keys=frozenset(preaccessed_storage_keys), + blob_versioned_hashes=blob_versioned_hashes, ) output = process_message_call(message, env) @@ -773,7 +828,9 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: create_cost = 0 access_list_cost = 0 - if isinstance(tx, (AccessListTransaction, FeeMarketTransaction)): + if isinstance( + tx, (AccessListTransaction, FeeMarketTransaction, BlobTransaction) + ): for _address, keys in tx.access_list: access_list_cost += TX_ACCESS_LIST_ADDRESS_COST access_list_cost += len(keys) * TX_ACCESS_LIST_STORAGE_KEY_COST @@ -829,6 +886,10 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: public_key = secp256k1_recover( r, s, tx.y_parity, signing_hash_1559(tx) ) + elif isinstance(tx, BlobTransaction): + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_4844(tx) + ) return Address(keccak256(public_key)[12:32]) @@ -957,6 +1018,40 @@ def signing_hash_1559(tx: FeeMarketTransaction) -> Hash32: ) +def signing_hash_4844(tx: BlobTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a EIP-4844 signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + 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 compute_header_hash(header: Header) -> Hash32: """ Computes the hash of a block header. diff --git a/src/ethereum/cancun/fork_types.py b/src/ethereum/cancun/fork_types.py index f6fa7572d23..4d032e3ae43 100644 --- a/src/ethereum/cancun/fork_types.py +++ b/src/ethereum/cancun/fork_types.py @@ -33,6 +33,7 @@ Address = Bytes20 Root = Hash32 +VersionedHash = Hash32 Bloom = Bytes256 @@ -103,8 +104,34 @@ class FeeMarketTransaction: s: U256 +@slotted_freezable +@dataclass +class BlobTransaction: + """ + The transaction type added in EIP-4844. + """ + + chain_id: U64 + nonce: U256 + max_priority_fee_per_gas: Uint + max_fee_per_gas: Uint + gas: Uint + to: Union[Bytes0, Address] + value: U256 + data: Bytes + access_list: Tuple[Tuple[Address, Tuple[Bytes32, ...]], ...] + max_fee_per_blob_gas: U256 + blob_versioned_hashes: Tuple[VersionedHash, ...] + y_parity: U256 + r: U256 + s: U256 + + Transaction = Union[ - LegacyTransaction, AccessListTransaction, FeeMarketTransaction + LegacyTransaction, + AccessListTransaction, + FeeMarketTransaction, + BlobTransaction, ] @@ -118,6 +145,8 @@ def encode_transaction(tx: Transaction) -> Union[LegacyTransaction, Bytes]: 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) else: raise Exception(f"Unable to encode transaction of type {type(tx)}") @@ -131,6 +160,8 @@ def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction: 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:]) else: raise InvalidBlock else: @@ -210,6 +241,8 @@ class Header: nonce: Bytes8 base_fee_per_gas: Uint withdrawals_root: Root + blob_gas_used: U64 + excess_blob_gas: U64 parent_beacon_block_root: Root diff --git a/src/ethereum/cancun/utils/message.py b/src/ethereum/cancun/utils/message.py index 7253eb3db36..878b01d8a46 100644 --- a/src/ethereum/cancun/utils/message.py +++ b/src/ethereum/cancun/utils/message.py @@ -16,7 +16,7 @@ from ethereum.base_types import U256, Bytes, Bytes0, Bytes32, Uint -from ..fork_types import Address +from ..fork_types import Address, VersionedHash from ..state import get_account from ..vm import Environment, Message from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS @@ -37,6 +37,7 @@ def prepare_message( preaccessed_storage_keys: FrozenSet[ Tuple[(Address, Bytes32)] ] = frozenset(), + blob_versioned_hashes: Tuple[VersionedHash, ...] = (), ) -> Message: """ Execute a transaction against the provided environment. @@ -69,6 +70,8 @@ def prepare_message( preaccessed_storage_keys: Storage keys that should be marked as accessed prior to the message call + blob_versioned_hashes: + A list of hashes obtained from the kzg commitments. Returns ------- @@ -112,4 +115,5 @@ def prepare_message( accessed_addresses=accessed_addresses, accessed_storage_keys=set(preaccessed_storage_keys), parent_evm=None, + blob_versioned_hashes=blob_versioned_hashes, ) diff --git a/src/ethereum/cancun/vm/__init__.py b/src/ethereum/cancun/vm/__init__.py index 95039b5a272..df602667492 100644 --- a/src/ethereum/cancun/vm/__init__.py +++ b/src/ethereum/cancun/vm/__init__.py @@ -19,7 +19,7 @@ from ethereum.base_types import U64, U256, Bytes, Bytes0, Bytes32, Uint from ethereum.crypto.hash import Hash32 -from ..fork_types import Address, Log +from ..fork_types import Address, Log, VersionedHash from ..state import State, TransientStorage, account_exists_and_is_empty from .precompiled_contracts import RIPEMD160_ADDRESS @@ -45,6 +45,7 @@ class Environment: state: State chain_id: U64 traces: List[dict] + excess_blob_gas: U64 @dataclass @@ -67,6 +68,7 @@ class Message: accessed_addresses: Set[Address] accessed_storage_keys: Set[Tuple[Address, Bytes32]] parent_evm: Optional["Evm"] + blob_versioned_hashes: Tuple[VersionedHash, ...] @dataclass diff --git a/src/ethereum/cancun/vm/exceptions.py b/src/ethereum/cancun/vm/exceptions.py index 2d54fbb5b36..ab3f4a54eda 100644 --- a/src/ethereum/cancun/vm/exceptions.py +++ b/src/ethereum/cancun/vm/exceptions.py @@ -126,3 +126,11 @@ class AddressCollision(ExceptionalHalt): """ pass + + +class KZGProofError(ExceptionalHalt): + """ + Raised when the point evaluation precompile can't verify a proof. + """ + + pass diff --git a/src/ethereum/cancun/vm/gas.py b/src/ethereum/cancun/vm/gas.py index d8ac4927287..ba54bf96320 100644 --- a/src/ethereum/cancun/vm/gas.py +++ b/src/ethereum/cancun/vm/gas.py @@ -14,11 +14,12 @@ from dataclasses import dataclass from typing import List, Tuple -from ethereum.base_types import U256, Uint +from ethereum.base_types import U64, U256, Uint from ethereum.trace import GasAndRefund, evm_trace -from ethereum.utils.numeric import ceil32 +from ethereum.utils.numeric import ceil32, taylor_exponential -from . import Evm +from ..fork_types import BlobTransaction, Header, Transaction +from . import Environment, Evm from .exceptions import OutOfGasError GAS_JUMPDEST = Uint(1) @@ -62,6 +63,13 @@ GAS_COLD_ACCOUNT_ACCESS = Uint(2600) GAS_WARM_ACCESS = Uint(100) GAS_INIT_CODE_WORD_COST = 2 +GAS_BLOBHASH_OPCODE = Uint(3) +GAS_POINT_EVALUATION = Uint(50000) + +TARGET_BLOB_GAS_PER_BLOCK = U64(393216) +GAS_PER_BLOB = Uint(2**17) +MIN_BLOB_GASPRICE = Uint(1) +BLOB_GASPRICE_UPDATE_FRACTION = Uint(3338477) @dataclass @@ -257,3 +265,87 @@ def init_code_cost(init_code_length: Uint) -> Uint: The gas to be charged for the init code. """ return GAS_INIT_CODE_WORD_COST * ceil32(init_code_length) // 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. + """ + parent_blob_gas = ( + parent_header.excess_blob_gas + parent_header.blob_gas_used + ) + if parent_blob_gas < TARGET_BLOB_GAS_PER_BLOCK: + return U64(0) + else: + return parent_blob_gas - TARGET_BLOB_GAS_PER_BLOCK + + +def calculate_total_blob_gas(tx: Transaction) -> Uint: + """ + 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 * len(tx.blob_versioned_hashes) + else: + return Uint(0) + + +def calculate_blob_gas_price(env: Environment) -> Uint: + """ + Calculate the blob gasprice for a block. + + Parameters + ---------- + env : + The execution environment. + + Returns + ------- + blob_gasprice: `Uint` + The blob gasprice. + """ + return taylor_exponential( + MIN_BLOB_GASPRICE, + Uint(env.excess_blob_gas), + BLOB_GASPRICE_UPDATE_FRACTION, + ) + + +def calculate_data_fee(env: Environment, tx: Transaction) -> Uint: + """ + Calculate the blob data fee for a transaction. + + Parameters + ---------- + env : + The execution environment. + tx : + The transaction for which the blob data fee is to be calculated. + + Returns + ------- + data_fee: `Uint` + The blob data fee. + """ + return calculate_total_blob_gas(tx) * calculate_blob_gas_price(env) diff --git a/src/ethereum/cancun/vm/instructions/__init__.py b/src/ethereum/cancun/vm/instructions/__init__.py index 4bc1b4efc23..e85592574d7 100644 --- a/src/ethereum/cancun/vm/instructions/__init__.py +++ b/src/ethereum/cancun/vm/instructions/__init__.py @@ -97,6 +97,8 @@ class Ops(enum.Enum): CHAINID = 0x46 SELFBALANCE = 0x47 BASEFEE = 0x48 + BLOBHASH = 0x49 + BLOBBASEFEE = 0x4A # Control Flow Ops STOP = 0x00 @@ -271,6 +273,8 @@ class Ops(enum.Enum): 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, diff --git a/src/ethereum/cancun/vm/instructions/environment.py b/src/ethereum/cancun/vm/instructions/environment.py index 613382757f5..2c4ccb265dc 100644 --- a/src/ethereum/cancun/vm/instructions/environment.py +++ b/src/ethereum/cancun/vm/instructions/environment.py @@ -12,7 +12,7 @@ Implementations of the EVM environment related instructions. """ -from ethereum.base_types import U256, Uint +from ethereum.base_types import U256, Bytes32, Uint from ethereum.crypto.hash import keccak256 from ethereum.utils.ensure import ensure from ethereum.utils.numeric import ceil32 @@ -25,12 +25,14 @@ 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, ) @@ -535,3 +537,54 @@ def base_fee(evm: Evm) -> None: # PROGRAM COUNTER evm.pc += 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 index < len(evm.message.blob_versioned_hashes): + blob_hash = evm.message.blob_versioned_hashes[index] + else: + blob_hash = Bytes32(b"\x00" * 32) + push(evm.stack, U256.from_be_bytes(blob_hash)) + + # PROGRAM COUNTER + evm.pc += 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.env) + push(evm.stack, U256(blob_base_fee)) + + # PROGRAM COUNTER + evm.pc += 1 diff --git a/src/ethereum/cancun/vm/instructions/system.py b/src/ethereum/cancun/vm/instructions/system.py index 9123cb577fd..b0a56a9a15a 100644 --- a/src/ethereum/cancun/vm/instructions/system.py +++ b/src/ethereum/cancun/vm/instructions/system.py @@ -123,6 +123,7 @@ def generic_create( accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, + blob_versioned_hashes=evm.message.blob_versioned_hashes, ) child_evm = process_create_message(child_message, evm.env) @@ -308,6 +309,7 @@ def generic_call( accessed_addresses=evm.accessed_addresses.copy(), accessed_storage_keys=evm.accessed_storage_keys.copy(), parent_evm=evm, + blob_versioned_hashes=evm.message.blob_versioned_hashes, ) child_evm = process_message(child_message, evm.env) diff --git a/src/ethereum/cancun/vm/precompiled_contracts/__init__.py b/src/ethereum/cancun/vm/precompiled_contracts/__init__.py index b1d9c464656..7ec48ca69bb 100644 --- a/src/ethereum/cancun/vm/precompiled_contracts/__init__.py +++ b/src/ethereum/cancun/vm/precompiled_contracts/__init__.py @@ -25,6 +25,7 @@ "ALT_BN128_MUL_ADDRESS", "ALT_BN128_PAIRING_CHECK_ADDRESS", "BLAKE2F_ADDRESS", + "POINT_EVALUATION_ADDRESS", ) ECRECOVER_ADDRESS = hex_to_address("0x01") @@ -36,3 +37,4 @@ 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") diff --git a/src/ethereum/cancun/vm/precompiled_contracts/mapping.py b/src/ethereum/cancun/vm/precompiled_contracts/mapping.py index 561ea192877..7bd3416b6b0 100644 --- a/src/ethereum/cancun/vm/precompiled_contracts/mapping.py +++ b/src/ethereum/cancun/vm/precompiled_contracts/mapping.py @@ -22,6 +22,7 @@ ECRECOVER_ADDRESS, IDENTITY_ADDRESS, MODEXP_ADDRESS, + POINT_EVALUATION_ADDRESS, RIPEMD160_ADDRESS, SHA256_ADDRESS, ) @@ -30,6 +31,7 @@ from .ecrecover import ecrecover from .identity import identity from .modexp import modexp +from .point_evaluation import point_evaluation from .ripemd160 import ripemd160 from .sha256 import sha256 @@ -43,4 +45,5 @@ ALT_BN128_MUL_ADDRESS: alt_bn128_mul, ALT_BN128_PAIRING_CHECK_ADDRESS: alt_bn128_pairing_check, BLAKE2F_ADDRESS: blake2f, + POINT_EVALUATION_ADDRESS: point_evaluation, } diff --git a/src/ethereum/cancun/vm/precompiled_contracts/point_evaluation.py b/src/ethereum/cancun/vm/precompiled_contracts/point_evaluation.py new file mode 100644 index 00000000000..65c3eb707ba --- /dev/null +++ b/src/ethereum/cancun/vm/precompiled_contracts/point_evaluation.py @@ -0,0 +1,76 @@ +""" +Ethereum Virtual Machine (EVM) POINT EVALUATION PRECOMPILED CONTRACT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the POINT EVALUATION precompiled contract. +""" +from eth2spec.deneb.mainnet import ( # type: ignore + KZGCommitment, + kzg_commitment_to_versioned_hash, + verify_kzg_proof, +) + +from ethereum.base_types import U256, Bytes +from ethereum.utils.ensure import ensure + +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 + + ensure(len(data) == 192, KZGProofError) + + versioned_hash = data[:32] + z = data[32:64] + y = data[64:96] + commitment = KZGCommitment(data[96:144]) + proof = data[144:192] + + # GAS + charge_gas(evm, GAS_POINT_EVALUATION) + + # OPERATION + # Verify commitment matches versioned_hash + ensure( + kzg_commitment_to_versioned_hash(commitment) == versioned_hash, + 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 + + ensure(kzg_proof_verification, 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/utils/numeric.py b/src/ethereum/utils/numeric.py index 94d66bec3ef..06f785bbbcc 100644 --- a/src/ethereum/utils/numeric.py +++ b/src/ethereum/utils/numeric.py @@ -165,3 +165,37 @@ def le_uint32_sequence_to_uint(sequence: Sequence[U32]) -> Uint: """ sequence_as_bytes = le_uint32_sequence_to_bytes(sequence) return Uint.from_le_bytes(sequence_as_bytes) + + +def taylor_exponential( + factor: Uint, numerator: Uint, denominator: Uint +) -> Uint: + """ + Approximates factor * e ** (numerator / denominator) using + Taylor expansion. + + Parameters + ---------- + factor : + The factor. + numerator : + The numerator of the exponential. + denominator : + The denominator of the exponential. + + Returns + ------- + output : `ethereum.base_types.Uint` + The approximation of factor * e ** (numerator / denominator). + + """ + i = 1 + output = 0 + numerator_accumulated = factor * denominator + while numerator_accumulated > 0: + output += numerator_accumulated + numerator_accumulated = (numerator_accumulated * numerator) // ( + denominator * i + ) + i += 1 + return output // denominator diff --git a/src/ethereum_spec_tools/evm_tools/fixture_loader.py b/src/ethereum_spec_tools/evm_tools/fixture_loader.py index 9bce828576a..7e39102abd8 100644 --- a/src/ethereum_spec_tools/evm_tools/fixture_loader.py +++ b/src/ethereum_spec_tools/evm_tools/fixture_loader.py @@ -296,6 +296,32 @@ def json_to_tx(self, raw: Any) -> Any: hex_to_u256(raw.get("s")), ] + # Cancun and beyond + if "maxFeePerBlobGas" in raw: + parameters.insert(0, U64(1)) + parameters.insert(2, hex_to_u256(raw.get("maxPriorityFeePerGas"))) + parameters.insert(3, hex_to_u256(raw.get("maxFeePerGas"))) + parameters.insert( + 8, self.json_to_access_list(raw.get("accessList")) + ) + parameters.insert(9, hex_to_u256(raw.get("maxFeePerBlobGas"))) + parameters.insert( + 10, + [ + hex_to_hash(blob_hash) + for blob_hash in raw.get("blobVersionedHashes") + ], + ) + + try: + return b"\x03" + rlp.encode( + self._module("fork_types").BlobTransaction(*parameters) + ) + except AttributeError as e: + raise UnsupportedTx( + b"\x03" + rlp.encode(parameters), str(e) + ) from e + # London and beyond if "maxFeePerGas" in raw and "maxPriorityFeePerGas" in raw: parameters.insert(0, U64(1)) @@ -423,4 +449,14 @@ def json_to_header(self, raw: Any) -> Any: withdrawals_root = self.hex_to_root(raw.get("withdrawalsRoot")) parameters.append(withdrawals_root) + if "excessBlobGas" in raw: + blob_gas_used = hex_to_u64(raw.get("blobGasUsed")) + parameters.append(blob_gas_used) + excess_blob_gas = hex_to_u64(raw.get("excessBlobGas")) + parameters.append(excess_blob_gas) + parent_beacon_block_root = self.hex_to_root( + raw.get("parentBeaconBlockRoot") + ) + parameters.append(parent_beacon_block_root) + return self.Header(*parameters) diff --git a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py index 5d6565acbb0..5b88d5d6543 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/__init__.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/__init__.py @@ -245,6 +245,9 @@ def environment(self, tx: Any, gas_available: Any) -> Any: kw_arguments["traces"] = [] + if self.is_after_fork("ethereum.cancun"): + kw_arguments["excess_blob_gas"] = self.env.excess_blob_gas + return self.vm.Environment(**kw_arguments) def tx_trie_set(self, trie: Any, index: Any, tx: Any) -> Any: @@ -354,6 +357,7 @@ def apply_body(self) -> None: accessed_addresses=set(), accessed_storage_keys=set(), parent_evm=None, + blob_versioned_hashes=(), ) system_tx_env = self.vm.Environment( @@ -370,6 +374,7 @@ def apply_body(self) -> None: state=self.alloc.state, chain_id=self.chain_id, traces=[], + excess_blob_gas=self.env.excess_blob_gas, ) self.interpreter.process_message_call( diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index 9d2e0916297..f4573227a0e 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Optional from ethereum import rlp -from ethereum.base_types import U256, Bytes32, Uint +from ethereum.base_types import U64, U256, Bytes32, Uint from ethereum.crypto.hash import Hash32, keccak256 from ethereum.utils.byte import left_pad_zero_bytes from ethereum.utils.hexadecimal import hex_to_bytes @@ -44,6 +44,7 @@ class Env: parent_ommers_hash: Optional[Hash32] ommers: Any parent_beacon_block_root: Optional[Hash32] + excess_blob_gas: Optional[U64] def __init__(self, t8n: Any, stdin: Optional[Dict] = None): if t8n.options.input_env == "stdin": @@ -69,8 +70,10 @@ def __init__(self, t8n: Any, stdin: Optional[Dict] = None): self.parent_beacon_block_root = Bytes32( hex_to_bytes(data["parentBeaconBlockRoot"]) ) + self.excess_blob_gas = parse_hex_or_int(data["excessBlobGas"], U64) else: self.parent_beacon_block_root = None + self.excess_blob_gas = None def read_base_fee_per_gas(self, data: Any, t8n: Any) -> 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 57520b46bd3..4473986c314 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py @@ -275,6 +275,9 @@ def sign_transaction(self, json_tx: Any) -> None: elif isinstance(tx_decoded, t8n.fork_types.FeeMarketTransaction): signing_hash = t8n.fork.signing_hash_1559(tx_decoded) v_addend = 0 + elif isinstance(tx_decoded, t8n.fork_types.BlobTransaction): + signing_hash = t8n.fork.signing_hash_4844(tx_decoded) + v_addend = 0 else: raise FatalException("Unknown transaction type") diff --git a/tests/cancun/test_rlp.py b/tests/cancun/test_rlp.py index ba1e6dabcd3..741743a9b52 100644 --- a/tests/cancun/test_rlp.py +++ b/tests/cancun/test_rlp.py @@ -106,6 +106,8 @@ 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), ) block = Block( diff --git a/whitelist.txt b/whitelist.txt index fc1c52b7cdd..f1ec2694af6 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -411,3 +411,10 @@ mcopy tload tstore + +KZG +BLOBHASH +eth2spec +Bytes48 +BLS +BLOBBASEFEE \ No newline at end of file