diff --git a/src/ethereum/osaka/blocks.py b/src/ethereum/osaka/blocks.py index 15208d4ad0..3fa125bec8 100644 --- a/src/ethereum/osaka/blocks.py +++ b/src/ethereum/osaka/blocks.py @@ -21,6 +21,7 @@ from .transactions import ( AccessListTransaction, BlobTransaction, + EofInitCodeTransaction, FeeMarketTransaction, LegacyTransaction, SetCodeTransaction, @@ -121,6 +122,8 @@ def encode_receipt(tx: Transaction, receipt: Receipt) -> Union[Bytes, Receipt]: return b"\x03" + rlp.encode(receipt) elif isinstance(tx, SetCodeTransaction): return b"\x04" + rlp.encode(receipt) + elif isinstance(tx, EofInitCodeTransaction): + return b"\x05" + rlp.encode(receipt) else: return receipt @@ -130,7 +133,7 @@ def decode_receipt(receipt: Union[Bytes, Receipt]) -> Receipt: Decodes a receipt. """ if isinstance(receipt, Bytes): - assert receipt[0] in (1, 2, 3, 4) + assert receipt[0] in (1, 2, 3, 4, 5) return rlp.decode_to(Receipt, receipt[1:]) else: return receipt diff --git a/src/ethereum/osaka/fork.py b/src/ethereum/osaka/fork.py index d39f1bd052..abe80de75e 100644 --- a/src/ethereum/osaka/fork.py +++ b/src/ethereum/osaka/fork.py @@ -52,6 +52,7 @@ from .transactions import ( AccessListTransaction, BlobTransaction, + EofInitCodeTransaction, FeeMarketTransaction, LegacyTransaction, SetCodeTransaction, @@ -428,7 +429,13 @@ def check_transaction( sender_account = get_account(block_env.state, sender_address) if isinstance( - tx, (FeeMarketTransaction, BlobTransaction, SetCodeTransaction) + tx, + ( + FeeMarketTransaction, + BlobTransaction, + SetCodeTransaction, + EofInitCodeTransaction, + ), ): if tx.max_fee_per_gas < tx.max_priority_fee_per_gas: raise InvalidBlock @@ -465,7 +472,9 @@ def check_transaction( else: blob_versioned_hashes = () - if isinstance(tx, (BlobTransaction, SetCodeTransaction)): + if isinstance( + tx, (BlobTransaction, SetCodeTransaction, EofInitCodeTransaction) + ): if not isinstance(tx.to, Address): raise InvalidBlock @@ -557,6 +566,7 @@ def process_system_transaction( transient_storage=TransientStorage(), blob_versioned_hashes=(), authorizations=(), + init_codes=None, index_in_block=None, tx_hash=None, traces=[], @@ -731,7 +741,7 @@ def process_transaction( encode_transaction(tx), ) - intrinsic_gas, calldata_floor_gas_cost = validate_transaction(tx) + intrinsic_gas, floor_gas_cost = validate_transaction(tx) ( sender, @@ -773,6 +783,7 @@ def process_transaction( FeeMarketTransaction, BlobTransaction, SetCodeTransaction, + EofInitCodeTransaction, ), ): for address, keys in tx.access_list: @@ -784,6 +795,11 @@ def process_transaction( if isinstance(tx, SetCodeTransaction): authorizations = tx.authorizations + if isinstance(tx, EofInitCodeTransaction): + init_codes = tx.init_codes + else: + init_codes = None + tx_env = vm.TransactionEnvironment( origin=sender, gas_price=effective_gas_price, @@ -793,6 +809,7 @@ def process_transaction( transient_storage=TransientStorage(), blob_versioned_hashes=blob_versioned_hashes, authorizations=authorizations, + init_codes=init_codes, index_in_block=index, tx_hash=get_transaction_hash(encode_transaction(tx)), traces=[], @@ -817,7 +834,7 @@ def process_transaction( # Transactions with less execution_gas_used than the floor pay at the # floor cost. - tx_gas_used = max(execution_gas_used, calldata_floor_gas_cost) + tx_gas_used = max(execution_gas_used, floor_gas_cost) tx_output.gas_left = tx.gas - tx_gas_used gas_refund_amount = tx_output.gas_left * effective_gas_price diff --git a/src/ethereum/osaka/transactions.py b/src/ethereum/osaka/transactions.py index 3e5adcfb46..91c5edfe3e 100644 --- a/src/ethereum/osaka/transactions.py +++ b/src/ethereum/osaka/transactions.py @@ -19,12 +19,14 @@ from .fork_types import Address, Authorization, VersionedHash TX_BASE_COST = Uint(21000) -FLOOR_CALLDATA_COST = Uint(10) -STANDARD_CALLDATA_TOKEN_COST = Uint(4) +TOTAL_COST_FLOOR_PER_TOKEN = Uint(10) +STANDARD_TOKEN_COST = Uint(4) TX_CREATE_COST = Uint(32000) TX_ACCESS_LIST_ADDRESS_COST = Uint(2400) TX_ACCESS_LIST_STORAGE_KEY_COST = Uint(1900) +MAX_INIT_CODE_COUNT = 256 + @slotted_freezable @dataclass @@ -130,12 +132,35 @@ class SetCodeTransaction: s: U256 +@slotted_freezable +@dataclass +class EofInitCodeTransaction: + """ + The transaction type added in EIP-7873. + """ + + 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, ...]], ...] + init_codes: Tuple[Bytes, ...] + y_parity: U256 + r: U256 + s: U256 + + Transaction = Union[ LegacyTransaction, AccessListTransaction, FeeMarketTransaction, BlobTransaction, SetCodeTransaction, + EofInitCodeTransaction, ] @@ -153,6 +178,8 @@ def encode_transaction(tx: Transaction) -> Union[LegacyTransaction, Bytes]: return b"\x03" + rlp.encode(tx) elif isinstance(tx, SetCodeTransaction): return b"\x04" + rlp.encode(tx) + elif isinstance(tx, EofInitCodeTransaction): + return b"\x05" + rlp.encode(tx) else: raise Exception(f"Unable to encode transaction of type {type(tx)}") @@ -170,6 +197,8 @@ def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction: return rlp.decode_to(BlobTransaction, tx[1:]) elif tx[0] == 4: return rlp.decode_to(SetCodeTransaction, tx[1:]) + elif tx[0] == 5: + return rlp.decode_to(EofInitCodeTransaction, tx[1:]) else: raise TransactionTypeError(tx[0]) else: @@ -211,6 +240,12 @@ def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]: """ from .vm.interpreter import MAX_CODE_SIZE + if isinstance(tx, EofInitCodeTransaction): + if len(tx.init_codes) == 0: + raise InvalidTransaction("Type 5 tx with no init codes") + if len(tx.init_codes) > MAX_INIT_CODE_COUNT: + raise InvalidTransaction("Type 5 tx with too many init codes") + intrinsic_gas, calldata_floor_gas_cost = calculate_intrinsic_cost(tx) if max(intrinsic_gas, calldata_floor_gas_cost) > tx.gas: raise InvalidTransaction("Insufficient gas") @@ -222,6 +257,28 @@ def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]: return intrinsic_gas, calldata_floor_gas_cost +def calculate_tokens_in_data(data: Bytes) -> Uint: + """ + Calculate the tokens in a certain data. + + Parameters + ---------- + data : + Data in which tokens are to be calculated. + + Returns + ------- + tokens_in_data : + Tokens in the data. + """ + zero_bytes = 0 + for byte in data: + if byte == 0: + zero_bytes += 1 + + return Uint(zero_bytes + (len(data) - zero_bytes) * 4) + + def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: """ Calculates the gas that is charged before execution is started. @@ -250,19 +307,27 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: """ from .vm.eoa_delegation import PER_EMPTY_ACCOUNT_COST from .vm.gas import init_code_cost + from .vm.interpreter import MAX_CODE_SIZE - zero_bytes = 0 - for byte in tx.data: - if byte == 0: - zero_bytes += 1 + tokens_in_tx = Uint(0) + + tokens_in_tx += calculate_tokens_in_data(tx.data) + + if isinstance(tx, EofInitCodeTransaction): + for init_code in tx.init_codes: + if len(init_code) == 0: + raise InvalidTransaction( + "Type 5 tx with zero-length init code" + ) + if len(init_code) > 2 * MAX_CODE_SIZE: + raise InvalidTransaction("Type 5 tx with too large init code") + + tokens_in_tx += calculate_tokens_in_data(init_code) - 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 - ) + floor_gas_cost = tokens_in_tx * TOTAL_COST_FLOOR_PER_TOKEN + TX_BASE_COST - data_cost = tokens_in_calldata * STANDARD_CALLDATA_TOKEN_COST + data_cost = tokens_in_tx * STANDARD_TOKEN_COST if tx.to == Bytes0(b""): create_cost = TX_CREATE_COST + init_code_cost(ulen(tx.data)) @@ -277,6 +342,7 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: FeeMarketTransaction, BlobTransaction, SetCodeTransaction, + EofInitCodeTransaction, ), ): for _address, keys in tx.access_list: @@ -295,7 +361,7 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: + access_list_cost + auth_cost ), - calldata_floor_gas_cost, + floor_gas_cost, ) @@ -365,6 +431,10 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: public_key = secp256k1_recover( r, s, tx.y_parity, signing_hash_7702(tx) ) + elif isinstance(tx, EofInitCodeTransaction): + public_key = secp256k1_recover( + r, s, tx.y_parity, signing_hash_7873(tx) + ) return Address(keccak256(public_key)[12:32]) @@ -560,6 +630,39 @@ def signing_hash_7702(tx: SetCodeTransaction) -> Hash32: ) +def signing_hash_7873(tx: EofInitCodeTransaction) -> Hash32: + """ + Compute the hash of a transaction used in a EIP-7873 signature. + + Parameters + ---------- + tx : + Transaction of interest. + + Returns + ------- + hash : `ethereum.crypto.hash.Hash32` + Hash of the transaction. + """ + return keccak256( + b"\x05" + + 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.init_codes, + ) + ) + ) + + def get_transaction_hash(tx: Union[Bytes, LegacyTransaction]) -> Hash32: """ Parameters diff --git a/src/ethereum/osaka/utils/address.py b/src/ethereum/osaka/utils/address.py index 290f22983e..abf1fd52e2 100644 --- a/src/ethereum/osaka/utils/address.py +++ b/src/ethereum/osaka/utils/address.py @@ -119,3 +119,30 @@ def compute_create2_contract_address( padded_address = left_pad_zero_bytes(canonical_address, 20) return Address(padded_address) + + +def compute_eof_tx_create_contract_address( + address: Address, salt: Bytes32 +) -> Address: + """ + Computes address of the new account that needs to be created, in the + EOF1 TXCREATE Opcode. + + Parameters + ---------- + address : + The address of the account that wants to create the new account. + salt : + Address generation salt. + + Returns + ------- + address: `ethereum.osaka.fork_types.Address` + The computed address of the new account. + """ + preimage = b"\xff" + address + salt + 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/osaka/vm/__init__.py b/src/ethereum/osaka/vm/__init__.py index f8c94bc8cc..1bcd3d9711 100644 --- a/src/ethereum/osaka/vm/__init__.py +++ b/src/ethereum/osaka/vm/__init__.py @@ -12,7 +12,6 @@ The abstract computer which runs the code stored in an `.fork_types.Account`. """ - from dataclasses import dataclass, field from typing import TYPE_CHECKING, List, Optional, Set, Tuple, Union @@ -112,6 +111,7 @@ class TransactionEnvironment: transient_storage: TransientStorage blob_versioned_hashes: Tuple[VersionedHash, ...] authorizations: Tuple[Authorization, ...] + init_codes: Optional[Tuple[Bytes, ...]] index_in_block: Optional[Uint] tx_hash: Optional[Hash32] traces: List[dict] diff --git a/src/ethereum/osaka/vm/instructions/__init__.py b/src/ethereum/osaka/vm/instructions/__init__.py index f2dd30a3f2..24ba6f5db3 100644 --- a/src/ethereum/osaka/vm/instructions/__init__.py +++ b/src/ethereum/osaka/vm/instructions/__init__.py @@ -240,6 +240,7 @@ class Ops(enum.Enum): # System Operations EOFCREATE = 0xEC + TXCREATE = 0xED RETURNCONTRACT = 0xEE CREATE = 0xF0 CALL = 0xF1 @@ -411,6 +412,7 @@ class Ops(enum.Enum): Ops.SWAPN: stack_instructions.swapn, Ops.EXCHANGE: stack_instructions.exchange, Ops.EOFCREATE: system_instructions.eof_create, + Ops.TXCREATE: system_instructions.eof_tx_create, Ops.RETURNCONTRACT: system_instructions.return_contract, Ops.CREATE: system_instructions.create, Ops.RETURN: system_instructions.return_, @@ -449,6 +451,7 @@ class Ops(enum.Enum): Ops.EXCHANGE, # System Operations Ops.EOFCREATE, + Ops.TXCREATE, Ops.RETURNDATALOAD, Ops.EXTCALL, Ops.EXTDELEGATECALL, @@ -641,6 +644,7 @@ class Ops(enum.Enum): Ops.CALLF: OpcodeStackItemCount(inputs=0, outputs=0), Ops.RETF: OpcodeStackItemCount(inputs=0, outputs=0), Ops.EOFCREATE: OpcodeStackItemCount(inputs=4, outputs=1), + Ops.TXCREATE: OpcodeStackItemCount(inputs=5, outputs=1), Ops.RETURNCONTRACT: OpcodeStackItemCount(inputs=2, outputs=0), Ops.CREATE: OpcodeStackItemCount(inputs=3, outputs=1), Ops.RETURN: OpcodeStackItemCount(inputs=2, outputs=0), diff --git a/src/ethereum/osaka/vm/instructions/system.py b/src/ethereum/osaka/vm/instructions/system.py index 6c151aeebe..72bf763c24 100644 --- a/src/ethereum/osaka/vm/instructions/system.py +++ b/src/ethereum/osaka/vm/instructions/system.py @@ -11,9 +11,12 @@ Implementations of the EVM system related instructions. """ -from ethereum_types.bytes import Bytes0 +from typing import Optional + +from ethereum_types.bytes import Bytes, Bytes0 from ethereum_types.numeric import U256, Uint, ulen +from ethereum.crypto.hash import keccak256 from ethereum.utils.numeric import ceil32 from ...fork_types import Address @@ -30,10 +33,12 @@ from ...utils.address import ( compute_contract_address, compute_create2_contract_address, + compute_eof_tx_create_contract_address, to_address, to_address_without_mask, ) from ...vm.eoa_delegation import access_delegation +from ...vm.exceptions import InvalidEof from .. import ( MAX_CODE_SIZE, Evm, @@ -1097,6 +1102,170 @@ def ext_staticcall(evm: Evm) -> None: evm.pc += Uint(1) +def generic_eof_create( + evm: Evm, + endowment: U256, + contract_address: Address, + init_code: Bytes, + input_offset: U256, + input_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 STACK_DEPTH_LIMIT, process_create_message + from ..eof import ContainerContext, Eof, get_eof_version + + 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 + ): + push(evm.stack, U256(0)) + return + + create_message_gas = max_message_call_gas(Uint(evm.gas_left)) + evm.gas_left -= create_message_gas + + 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 + + try: + metadata = metadata_from_container( + init_code, + validate=True, + context=ContainerContext.INIT, + ) + except InvalidEof: + push(evm.stack, U256(0)) + return + + call_data = memory_read_bytes(evm.memory, input_offset, input_size) + + eof = Eof( + version=get_eof_version(init_code), + container=init_code, + metadata=metadata, + ) + + evm.accessed_addresses.add(contract_address) + + 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=call_data, + code=init_code, + 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(), + parent_evm=evm, + eof=eof, + ) + 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 eof_tx_create(evm: Evm) -> None: + """ + Creates a new account with associated code. Introduced in EOF1 as + per EIP-7873. + + Parameters + ---------- + evm : + The current EVM frame. + """ + # STACK + tx_init_code_hash = pop(evm.stack).to_be_bytes32() + salt = pop(evm.stack).to_be_bytes32() + input_offset = pop(evm.stack) + input_size = pop(evm.stack) + value = pop(evm.stack) + + # GAS + extend_memory = calculate_gas_extend_memory( + evm.memory, [(input_offset, input_size)] + ) + call_data_words = ceil32(Uint(input_size)) // Uint(32) + init_code_gas = init_code_cost(Uint(input_size)) + charge_gas( + evm, + GAS_CREATE + + GAS_KECCAK256_WORD * call_data_words + + extend_memory.cost + + init_code_gas, + ) + + # OPERATION + if evm.message.is_static: + raise WriteInStaticContext("TX CREATE in static mode") + + evm.memory += b"\x00" * extend_memory.expand_by + + contract_address = compute_eof_tx_create_contract_address( + evm.message.current_target, salt + ) + + # tx_env.init_codes are None for all but Type 5 transactions + if evm.message.tx_env.init_codes is None: + push(evm.stack, U256(0)) + else: + # If none of the init codes in the tx match, the + # code could still be None + code: Optional[Bytes] = None + + for init_code in evm.message.tx_env.init_codes: + if keccak256(init_code) == tx_init_code_hash: + code = init_code + break + + # None of the init codes in the transaction match the + # provided init code hash + if code is None: + push(evm.stack, U256(0)) + else: + generic_eof_create( + evm=evm, + endowment=value, + contract_address=contract_address, + init_code=code, + input_offset=input_offset, + input_size=input_size, + ) + + # PROGRAM COUNTER + evm.pc += Uint(1) + + def eof_create(evm: Evm) -> None: """ Creates a new account with associated code for EOF. @@ -1106,9 +1275,6 @@ def eof_create(evm: Evm) -> None: evm : The current EVM frame. """ - from ...vm.interpreter import STACK_DEPTH_LIMIT, process_create_message - from ..eof import ContainerContext, Eof, get_eof_version - assert evm.eof is not None # STACK @@ -1141,78 +1307,20 @@ def eof_create(evm: Evm) -> None: raise WriteInStaticContext("EOFCREATE in static mode") evm.memory += b"\x00" * extend_memory.expand_by - state = evm.message.block_env.state - - sender_address = evm.message.current_target - sender = get_account(state, sender_address) - contract_address = compute_create2_contract_address( evm.message.current_target, salt, bytearray(init_container), ) - evm.accessed_addresses.add(contract_address) - create_message_gas = max_message_call_gas(Uint(evm.gas_left)) - evm.gas_left -= create_message_gas - - if ( - sender.balance < value - 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)) - elif account_has_code_or_nonce(state, contract_address): - increment_nonce(state, evm.message.current_target) - push(evm.stack, U256(0)) - else: - call_data = memory_read_bytes(evm.memory, input_offset, input_size) - - increment_nonce(state, evm.message.current_target) - - metadata = metadata_from_container( - init_container, - validate=True, - context=ContainerContext.INIT, - ) - eof = Eof( - version=get_eof_version(init_container), - container=init_container, - metadata=metadata, - ) - - 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=value, - data=call_data, - code=init_container, - 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(), - parent_evm=evm, - eof=eof, - ) - 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) - ) + generic_eof_create( + evm=evm, + endowment=value, + contract_address=contract_address, + init_code=init_container, + input_offset=input_offset, + input_size=input_size, + ) # PROGRAM COUNTER evm.pc += Uint(2) diff --git a/whitelist.txt b/whitelist.txt index fa03d31a5f..b92d517923 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -496,4 +496,6 @@ EXTSTATICCALL CALLEE subcontainer -subcontainers \ No newline at end of file +subcontainers + +TXCREATE \ No newline at end of file