Skip to content
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

Implement EIP-4844 #867

Merged
merged 4 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ install_requires =
pycryptodome>=3,<4
coincurve>=18,<19
typing_extensions>=4
eth2spec @ git+https://github.com/ethereum/consensus-specs.git@7402712e4faebcb9cd87370d91acf638ae168949

[options.package_data]
ethereum =
Expand Down
8 changes: 8 additions & 0 deletions src/ethereum/base_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
112 changes: 102 additions & 10 deletions src/ethereum/cancun/fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
TX_DATA_COST_PER_ZERO,
AccessListTransaction,
Address,
BlobTransaction,
Block,
Bloom,
FeeMarketTransaction,
Expand All @@ -43,6 +44,7 @@
Receipt,
Root,
Transaction,
VersionedHash,
Withdrawal,
decode_transaction,
encode_transaction,
Expand All @@ -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_data_fee,
calculate_excess_blob_gas,
get_blob_gasprice,
get_total_blob_gas,
init_code_cost,
)
from .vm.interpreter import MAX_CODE_SIZE, process_message_call

BASE_FEE_MAX_CHANGE_DENOMINATOR = 8
Expand All @@ -74,6 +82,8 @@
"0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02"
)
SYSTEM_TRANSACTION_GAS = Uint(30000000)
MAX_BLOB_GAS_PER_BLOCK = 786432
VERSIONED_HASH_VERSION_KZG = b"\x01"


@dataclass
Expand Down Expand Up @@ -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)
(
Expand All @@ -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),
Expand All @@ -194,13 +208,15 @@ 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)
ensure(state_root(state) == block.header.state_root, InvalidBlock)
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:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -403,6 +419,8 @@ def make_receipt(
return b"\x01" + rlp.encode(receipt)
if isinstance(tx, FeeMarketTransaction):
return b"\x02" + rlp.encode(receipt)
if isinstance(tx, BlobTransaction):
return b"\x03" + rlp.encode(receipt)
else:
return receipt
SamWilsn marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -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]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we make a dataclass for this? It's getting really unwieldy.

"""
Executes a block.

Expand Down Expand Up @@ -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
-------
Expand All @@ -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]]
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -566,7 +591,9 @@ def apply_body(
)

block_logs += logs
blob_gas_used += get_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)
Expand All @@ -586,6 +613,7 @@ def apply_body(
block_logs_bloom,
state,
root(withdrawals_trie),
blob_gas_used,
)


Expand Down Expand Up @@ -623,32 +651,55 @@ 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
gurukamath marked this conversation as resolved.
Show resolved Hide resolved
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 >= get_blob_gasprice(env), InvalidBlock)

max_gas_fee += get_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

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)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we make (and backport) a has_access_list function? Not specifically for this pull request, just in general.

Copy link
Collaborator Author

@gurukamath gurukamath Feb 5, 2024

Choose a reason for hiding this comment

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

Would probably make the maintaining of these properties easier.

Edit: Apparently, mypy is unable to infer the narrowing of types if done inside another function.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You need to use User-Defined Type Guards.

):
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,
Expand All @@ -658,6 +709,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)
Expand Down Expand Up @@ -773,7 +825,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
Expand Down Expand Up @@ -829,6 +883,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])

Expand Down Expand Up @@ -957,6 +1015,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.
gurukamath marked this conversation as resolved.
Show resolved Hide resolved

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.
Expand Down
35 changes: 34 additions & 1 deletion src/ethereum/cancun/fork_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

Address = Bytes20
Root = Hash32
VersionedHash = Hash32

Bloom = Bytes256

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


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

Expand All @@ -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:
Expand Down Expand Up @@ -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


Expand Down
Loading
Loading