Skip to content

Commit

Permalink
Merge pull request #1000 from ethereum/eips/prague/eip-7702
Browse files Browse the repository at this point in the history
Implement EIP-7702
  • Loading branch information
petertdavies authored Sep 19, 2024
2 parents 10125b1 + 188c9aa commit d53bf9a
Show file tree
Hide file tree
Showing 17 changed files with 471 additions and 40 deletions.
5 changes: 4 additions & 1 deletion src/ethereum/prague/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
BlobTransaction,
FeeMarketTransaction,
LegacyTransaction,
SetCodeTransaction,
Transaction,
)

Expand Down Expand Up @@ -124,6 +125,8 @@ def encode_receipt(tx: Transaction, receipt: Receipt) -> Union[Bytes, Receipt]:
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

Expand All @@ -133,7 +136,7 @@ def decode_receipt(receipt: Union[Bytes, Receipt]) -> Receipt:
Decodes a receipt.
"""
if isinstance(receipt, Bytes):
assert receipt[0] in (b"\x01", b"\x02", b"\x03")
assert receipt[0] in (1, 2, 3, 4)
return rlp.decode_to(Receipt, receipt[1:])
else:
return receipt
90 changes: 82 additions & 8 deletions src/ethereum/prague/fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from . import vm
from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt
from .bloom import logs_bloom
from .fork_types import Address, Bloom, Root, VersionedHash
from .fork_types import Address, Authorization, Bloom, Root, VersionedHash
from .requests import (
parse_consolidation_requests_from_system_tx,
parse_deposit_requests_from_receipt,
Expand Down Expand Up @@ -55,6 +55,7 @@
BlobTransaction,
FeeMarketTransaction,
LegacyTransaction,
SetCodeTransaction,
Transaction,
decode_transaction,
encode_transaction,
Expand All @@ -63,6 +64,7 @@
from .utils.hexadecimal import hex_to_address
from .utils.message import prepare_message
from .vm import Message
from .vm.eoa_delegation import PER_EMPTY_ACCOUNT_COST, is_valid_delegation
from .vm.gas import (
calculate_blob_gas_price,
calculate_data_fee,
Expand Down Expand Up @@ -407,7 +409,9 @@ def check_transaction(

sender_account = get_account(state, sender_address)

if isinstance(tx, (FeeMarketTransaction, BlobTransaction)):
if isinstance(
tx, (FeeMarketTransaction, BlobTransaction, SetCodeTransaction)
):
if tx.max_fee_per_gas < tx.max_priority_fee_per_gas:
raise InvalidBlock
if tx.max_fee_per_gas < base_fee_per_gas:
Expand All @@ -426,8 +430,6 @@ def check_transaction(
max_gas_fee = tx.gas * tx.gas_price

if isinstance(tx, BlobTransaction):
if not isinstance(tx.to, Address):
raise InvalidBlock
if len(tx.blob_versioned_hashes) == 0:
raise InvalidBlock
for blob_versioned_hash in tx.blob_versioned_hashes:
Expand All @@ -441,11 +443,22 @@ def check_transaction(
blob_versioned_hashes = tx.blob_versioned_hashes
else:
blob_versioned_hashes = ()

if isinstance(tx, (BlobTransaction, SetCodeTransaction)):
if not isinstance(tx.to, Address):
raise InvalidBlock

if isinstance(tx, SetCodeTransaction):
if not any(tx.authorizations):
raise InvalidBlock

if sender_account.nonce != tx.nonce:
raise InvalidBlock
if sender_account.balance < max_gas_fee + tx.value:
raise InvalidBlock
if sender_account.code != bytearray():
if sender_account.code != bytearray() and not is_valid_delegation(
sender_account.code
):
raise InvalidBlock

return sender_address, effective_gas_price, blob_versioned_hashes
Expand Down Expand Up @@ -589,6 +602,7 @@ def process_system_transaction(
accessed_addresses=set(),
accessed_storage_keys=set(),
parent_evm=None,
authorizations=(),
)

system_tx_env = vm.Environment(
Expand Down Expand Up @@ -919,13 +933,23 @@ def process_transaction(
preaccessed_storage_keys = set()
preaccessed_addresses.add(env.coinbase)
if isinstance(
tx, (AccessListTransaction, FeeMarketTransaction, BlobTransaction)
tx,
(
AccessListTransaction,
FeeMarketTransaction,
BlobTransaction,
SetCodeTransaction,
),
):
for address, keys in tx.access_list:
preaccessed_addresses.add(address)
for key in keys:
preaccessed_storage_keys.add((address, key))

authorizations: Tuple[Authorization, ...] = ()
if isinstance(tx, SetCodeTransaction):
authorizations = tx.authorizations

message = prepare_message(
sender,
tx.to,
Expand All @@ -935,6 +959,7 @@ def process_transaction(
env,
preaccessed_addresses=frozenset(preaccessed_addresses),
preaccessed_storage_keys=frozenset(preaccessed_storage_keys),
authorizations=authorizations,
)

output = process_message_call(message, env)
Expand Down Expand Up @@ -1014,13 +1039,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint:

access_list_cost = 0
if isinstance(
tx, (AccessListTransaction, FeeMarketTransaction, BlobTransaction)
tx,
(
AccessListTransaction,
FeeMarketTransaction,
BlobTransaction,
SetCodeTransaction,
),
):
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

return Uint(TX_BASE_COST + data_cost + create_cost + access_list_cost)
auth_cost = 0
if isinstance(tx, SetCodeTransaction):
auth_cost += PER_EMPTY_ACCOUNT_COST * len(tx.authorizations)

return Uint(
TX_BASE_COST + data_cost + create_cost + access_list_cost + auth_cost
)


def recover_sender(chain_id: U64, tx: Transaction) -> Address:
Expand Down Expand Up @@ -1076,6 +1113,10 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address:
public_key = secp256k1_recover(
r, s, tx.y_parity, signing_hash_4844(tx)
)
elif isinstance(tx, SetCodeTransaction):
public_key = secp256k1_recover(
r, s, tx.y_parity, signing_hash_7702(tx)
)
except InvalidSignature as e:
raise InvalidBlock from e

Expand Down Expand Up @@ -1240,6 +1281,39 @@ def signing_hash_4844(tx: BlobTransaction) -> Hash32:
)


def signing_hash_7702(tx: SetCodeTransaction) -> Hash32:
"""
Compute the hash of a transaction used in a EIP-7702 signature.
Parameters
----------
tx :
Transaction of interest.
Returns
-------
hash : `ethereum.crypto.hash.Hash32`
Hash of the transaction.
"""
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 compute_header_hash(header: Header) -> Hash32:
"""
Computes the hash of a block header.
Expand Down
16 changes: 16 additions & 0 deletions src/ethereum/prague/fork_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from .. import rlp
from ..base_types import (
U64,
U256,
Bytes,
Bytes20,
Expand Down Expand Up @@ -66,3 +67,18 @@ def encode_account(raw_account_data: Account, storage_root: Bytes) -> Bytes:
keccak256(raw_account_data.code),
)
)


@slotted_freezable
@dataclass
class Authorization:
"""
The authorization for a set code transaction.
"""

chain_id: U64
address: Address
nonce: U64
y_parity: U256
r: U256
s: U256
32 changes: 30 additions & 2 deletions src/ethereum/prague/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from dataclasses import dataclass
from typing import Tuple, Union

from ethereum.exceptions import InvalidBlock

from .. import rlp
from ..base_types import (
U64,
Expand All @@ -16,8 +18,7 @@
Uint,
slotted_freezable,
)
from ..exceptions import InvalidBlock
from .fork_types import Address, VersionedHash
from .fork_types import Address, Authorization, VersionedHash

TX_BASE_COST = 21000
TX_DATA_COST_PER_NON_ZERO = 16
Expand Down Expand Up @@ -109,11 +110,34 @@ class BlobTransaction:
s: U256


@slotted_freezable
@dataclass
class SetCodeTransaction:
"""
The transaction type added in EIP-7702.
"""

chain_id: U64
nonce: U64
max_priority_fee_per_gas: Uint
max_fee_per_gas: Uint
gas: Uint
to: Address
value: U256
data: Bytes
access_list: Tuple[Tuple[Address, Tuple[Bytes32, ...]], ...]
authorizations: Tuple[Authorization, ...]
y_parity: U256
r: U256
s: U256


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


Expand All @@ -129,6 +153,8 @@ def encode_transaction(tx: Transaction) -> Union[LegacyTransaction, Bytes]:
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)}")

Expand All @@ -144,6 +170,8 @@ def decode_transaction(tx: Union[LegacyTransaction, Bytes]) -> Transaction:
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 InvalidBlock
else:
Expand Down
6 changes: 5 additions & 1 deletion src/ethereum/prague/utils/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from ethereum.base_types import U256, Bytes, Bytes0, Bytes32, Uint

from ..fork_types import Address
from ..fork_types import Address, Authorization
from ..state import get_account
from ..vm import Environment, Message
from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS
Expand All @@ -37,6 +37,7 @@ def prepare_message(
preaccessed_storage_keys: FrozenSet[
Tuple[(Address, Bytes32)]
] = frozenset(),
authorizations: Tuple[Authorization, ...] = (),
) -> Message:
"""
Execute a transaction against the provided environment.
Expand Down Expand Up @@ -69,6 +70,8 @@ def prepare_message(
preaccessed_storage_keys:
Storage keys that should be marked as accessed prior to the message
call
authorizations:
Authorizations that should be applied to the message call.
Returns
-------
Expand Down Expand Up @@ -112,4 +115,5 @@ def prepare_message(
accessed_addresses=accessed_addresses,
accessed_storage_keys=set(preaccessed_storage_keys),
parent_evm=None,
authorizations=authorizations,
)
3 changes: 2 additions & 1 deletion src/ethereum/prague/vm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from ethereum.crypto.hash import Hash32

from ..blocks import Log
from ..fork_types import Address, VersionedHash
from ..fork_types import Address, Authorization, VersionedHash
from ..state import State, TransientStorage, account_exists_and_is_empty
from .precompiled_contracts import RIPEMD160_ADDRESS

Expand Down Expand Up @@ -71,6 +71,7 @@ class Message:
accessed_addresses: Set[Address]
accessed_storage_keys: Set[Tuple[Address, Bytes32]]
parent_evm: Optional["Evm"]
authorizations: Tuple[Authorization, ...]


@dataclass
Expand Down
Loading

0 comments on commit d53bf9a

Please sign in to comment.