Skip to content

Implement EIP-7873 and EIP-5920 #1180

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/ethereum/osaka/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .transactions import (
AccessListTransaction,
BlobTransaction,
EofInitCodeTransaction,
FeeMarketTransaction,
LegacyTransaction,
SetCodeTransaction,
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transaction code 0x05 is for EIP-7702. This EIP specifies 0x06 as the code for this transaction type. This incongruity also occurs in transactions.py on lines 181, and 648.

else:
return receipt

Expand All @@ -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
25 changes: 21 additions & 4 deletions src/ethereum/osaka/fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from .transactions import (
AccessListTransaction,
BlobTransaction,
EofInitCodeTransaction,
FeeMarketTransaction,
LegacyTransaction,
SetCodeTransaction,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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=[],
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -773,6 +783,7 @@ def process_transaction(
FeeMarketTransaction,
BlobTransaction,
SetCodeTransaction,
EofInitCodeTransaction,
),
):
for address, keys in tx.access_list:
Expand All @@ -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,
Expand All @@ -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=[],
Expand All @@ -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
Expand Down
127 changes: 115 additions & 12 deletions src/ethereum/osaka/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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, ...]], ...]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a heads up, this will end up needing to be refactored from Tuple[Address, Tuple[Bytes32, ...]] to Access after Osaka merges the upstream changes. Just noting this, since this branch is behind upstream changes to master/prague that include this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Yeah. There will be a larger re-basing effort on all our EOF related branches soon

init_codes: Tuple[Bytes, ...]
y_parity: U256
r: U256
s: U256


Transaction = Union[
LegacyTransaction,
AccessListTransaction,
FeeMarketTransaction,
BlobTransaction,
SetCodeTransaction,
EofInitCodeTransaction,
]


Expand All @@ -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)}")

Expand All @@ -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:
Expand Down Expand Up @@ -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")
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should either be a comment referencing the EIP definition for MAX_INITCODE_SIZE, or MAX_INITCODE_SIZE should just be defined in the same place as MAX_CODE_SIZE and referenced from there. IMO this would be more readable that way.

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))
Expand All @@ -277,6 +342,7 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]:
FeeMarketTransaction,
BlobTransaction,
SetCodeTransaction,
EofInitCodeTransaction,
),
):
for _address, keys in tx.access_list:
Expand All @@ -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,
)


Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions src/ethereum/osaka/utils/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EIP specifies keccak256(0xff || sender32 || salt)[12:]. This form is equivalent, but I think it would be more readable to anyone who isn't a python native if it's [12:] ... though it dovetails well with the next line. More of a "to consider" than "defect".

padded_address = left_pad_zero_bytes(canonical_address, 20)

return Address(padded_address)
2 changes: 1 addition & 1 deletion src/ethereum/osaka/vm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down
Loading