From 68f565ab4f8ef5114dba09a35d94ad32ecdb409d Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Mon, 30 Jan 2023 00:04:45 +0100 Subject: [PATCH 01/25] Add EIP: Transition to SSZ --- EIPS/eip-6404.md | 1000 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1000 insertions(+) create mode 100644 EIPS/eip-6404.md diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md new file mode 100644 index 00000000000000..2aafa3b6406caf --- /dev/null +++ b/EIPS/eip-6404.md @@ -0,0 +1,1000 @@ +--- +eip: 6404 +title: Transition to SSZ +description: Unifies the CL and EL block representations +author: Etan Kissling (@etan-status) +discussions-to: https://ethereum-magicians.org/t/eip-6404-transition-to-ssz/12783 +status: Draft +type: Standards Track +category: Core +created: 2023-01-30 +requires: 155, 658, 1559, 2718, 2930, 4844, 4895 +--- + +## Abstract + +This EIP aligns the definition of the EL block with the CL `ExecutionPayload` structure. + +## Motivation + +While the CL `ExecutionPayload` and the EL block structure map to each other, they are differently encoded. This EIP aims to align the encoding between them, taking advantage of the more modern SSZ format used by the CL. This brings several advantages: + +1. **Reducing complexity:** Merkle-Patricia Tries (MPT) are hard to work with. Replacing them with SSZ leaves only the state trie in the legacy MPT format. + +2. **Better for smart contracts:** The SSZ format is optimized for production and verification of merkle proofs. It allows proving specific fields of containers and allows chunked processing, e.g., to support handling transactions that do not fit into calldata. + +3. **Better for light clients:** Light clients with access to the CL `ExecutionPayload` no longer need to obtain the matching EL block header to verify proofs rooted in `transactions_root` or `withdrawals_root`. + +4. **Reducing ambiguity:** The names `transactions_root` and `withdrawals_root` are currently used by both EL and CL to refer to different roots. The EL refers to a MPT root, the CL refers to a SSZ root. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +### SSZ + +#### `Optional[T]` + +A new [SSZ type](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/ssz/simple-serialize.md) is introduced to represent `Optional[T]` values. + +- If value is `None`, serialize as `[]`, and merkleize as `List[T, 1]` with length `0`. +- If value is not `None`, serialize as `T`, and merkleize as `List[T, 1]` with length `1`. +- Serialize `Optional[T]` as variable-size object (create offset-table entry in enclosing containers). + +#### Fixed capacity `Container` (TBD) + +The merkleization for SSZ `Container` structures generates a tree of minimum depth to contain all leaves. This is problematic whenever the number of leaves changes to require a different power of 2, as all `GeneralizedIndex` values change. + +SSZ `Container` structures should be annotated with a maximum capacity, similar to SSZ `List`. Unused space would be zero-extended accordingly. + +TBD + +### Transactions + +This design proposes a single normalized `Transaction` type. To include a transaction from an earlier type into a block, it needs to be migrated using `upgrade_signed_rlp_transaction_to_ssz`. For the purpose of signature validation, the originally signed transaction hash can be recomputed using `get_transaction_sighash`. For applications that need a perpetual identifier, `get_transaction_id` can be used. + +#### Hash version + +For the purpose of transaction signature validation, it is necessary to recover the originally signed transaction hash. A new enumeration is introduced to indicate the original transaction hashing method. Note that this is different from the [EIP-2718](./eip-2718.md) transaction type, as legacy transactions may have been signed using different methods. The [EIP-2718](./eip-2718.md) transaction type is still used to encapsule different transaction formats on the network. + +| Name | SSZ equivalent | Description | +| - | - | - | +| `TxHashVersion` | `uint8` | Original hashing method used for signing the transaction + +| Name | Value | Description | +| - | - | - | +| `TX_HASH_VERSION_LEGACY` | `TxHashVersion(0x00)` | Replayable [`LegacyTransaction`](./eip-2718.md#transactions) | +| `TX_HASH_VERSION_EIP155` | `TxHashVersion(0x01)` | [EIP-155](./eip-155.md) `LegacyTransaction` | +| `TX_HASH_VERSION_EIP2930` | `TxHashVersion(0x02)` | [EIP-2930](./eip-2930.md#definitions) transaction | +| `TX_HASH_VERSION_EIP1559` | `TxHashVersion(0x03)` | [EIP-1559](./eip-1559.md#specification) transaction | +| `TX_HASH_VERSION_EIP4844` | `TxHashVersion(0x04)` | [EIP-4844](./eip-4844.md#parameters) transaction | + +#### Execution block changes (Transactions) + +A new `Transaction` SSZ container is introduced to represent transactions. + +| Name | SSZ equivalent | +| - | - | +| [`BLSFieldElement`](./eip-4844.md#type-aliases) | `uint256` | +| [`VersionedHash`](./eip-4844.md#type-aliases) | `Bytes32` | +| [`KZGCommitment`](./eip-4844.md#type-aliases) | `Bytes48` | +| [`KZGProof`](./eip-4844.md#type-aliases) | `Bytes48` | + +| Name | Value | +| - | - | +| [`FIELD_ELEMENTS_PER_BLOB`](./eip-4844.md#parameters) | `uint64(2**12)` (= 4,096) | +| [`MAX_VERSIONED_HASHES_LIST_SIZE`](./eip-4844.md#parameters) | `uint64(2**24)` (= 16,777,216) | +| [`MAX_CALLDATA_SIZE`](./eip-4844.md#parameters) | `uint64(2**24)` (= 16,777,216) | +| [`MAX_ACCESS_LIST_SIZE`](./eip-4844.md#parameters) | `uint64(2**24)` (= 16,777,216) | +| [`MAX_ACCESS_LIST_STORAGE_KEYS`](./eip-4844.md#parameters) | `uint64(2**24)` (= 16,777,216) | +| [`MAX_TX_WRAP_KZG_COMMITMENTS`](./eip-4844.md#parameters) | `uint64(2**24)` (= 16,777,216) | +| [`LIMIT_BLOBS_PER_TX`](./eip-4844.md#parameters) | `uint64(2**24)` (= 16,777,216) | + +```python +class AccessTuple(Container): + address: Address + storage_keys: List[Hash, MAX_ACCESS_LIST_STORAGE_KEYS] + +class Transaction(Container): + chain_id: uint64 # EIP-155 + nonce: uint64 + max_priority_fee_per_gas: uint256 # EIP-1559 + max_fee_per_gas: uint256 # aka `gasprice` + gas_limit: uint64 # aka `startgas` + to: Optional[Address] # None: deploy contract + value: uint256 + data: ByteList[MAX_CALLDATA_SIZE] + access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE] # EIP-2930 + blob_versioned_hashes: List[VersionedHash, MAX_VERSIONED_HASHES_LIST_SIZE] # EIP-4844 + +class ECDSASignature(Container): + y_parity: boolean # EIP-2930 + r: uint256 + s: uint256 + +class SignedTransaction(Container): + tx: Transaction + signature: ECDSASignature + hash_version: TxHashVersion + +class BlobTransactionExtension(Container): + blob_kzgs: List[KZGCommitment, MAX_TX_WRAP_KZG_COMMITMENTS] + blobs: List[Vector[BLSFieldElement, FIELD_ELEMENTS_PER_BLOB], LIMIT_BLOBS_PER_TX] + kzg_aggregated_proof: KZGProof + +class TransactionPayload(Container): + signed_tx: SignedTransaction + blob: Optional[BlobTransactionExtension] # EIP-4844 +``` + +On the network, `TransactionPayload` is preceded by a single byte, [`BLOB_TX_TYPE`](./eip-4844.md#parameters) to distinguish from other transaction formats according to [EIP-2718](./eip-2718.md), regardless of the inner `signed_tx.hash_version`. + +The `blocks.transactions` list remains RLP encoded: + +```python +block.transactions = RLP([ + SSZ.encode(transaction_0), + SSZ.encode(transaction_1), + SSZ.encode(transaction_2), + ... +]) +``` + +#### Consensus `ExecutionPayload` changes (Transactions) + +The [CL `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) is updated to replace the existing [CL `Transaction`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#custom-types) container with the new `SignedTransaction` container. + +| Name | Value | Description | +| - | - | - | +| [`MAX_TRANSACTIONS_PER_PAYLOAD`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#execution) | `uint64(2**20)` (= 1,048,576) | Maximum amount of transactions allowed in each block | + +```python +class ExecutionPayload(Container): + ... + transactions: List[SignedTransaction, MAX_TRANSACTIONS_PER_PAYLOAD] # Formerly, `Transaction` + ... +``` + +#### Execution block header changes (Transactions) + +The execution block header's `transactions_root` is updated to the `transactions_root` definition of the [consensus-specs `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader). + +To compute the new `transactions_root`, the list of individual `SignedTransaction` containers is represented as a SSZ `List`. + +```python +block_header.transactions_root == hash_tree_root(List[SignedTransaction, MAX_TRANSACTIONS_PER_PAYLOAD]( + transaction_0, + transaction_1, + transaction_2, + ... +)) == hash_tree_root(List[Root, MAX_TRANSACTIONS_PER_PAYLOAD]( + hash_tree_root(transaction_0), + hash_tree_root(transaction_1), + hash_tree_root(transaction_2), + ... +)) +``` + +##### Transaction validation + +```python +def get_transaction_sighash(tx: Transaction, hash_version: TxHashVersion) -> bytes: + if hash_version == TX_HASH_VERSION_EIP4844: + return hash_tree_root(tx) + + assert len(tx.blob_versioned_hashes) == 0 + + if hash_version == TX_HASH_VERSION_EIP1559: + schema = ( + (big_endian_int, tx.chain_id), + (big_endian_int, tx.nonce), + (big_endian_int, tx.max_priority_fee_per_gas), + (big_endian_int, tx.max_fee_per_gas), + (big_endian_int, tx.gas_limit), + (binary, tx.to if tx.to is not None else []), + (big_endian_int, tx.value), + (binary, tx.data), + (List([Binary[20, 20], List([Binary[32, 32]])]), [ + (access_tuple.address, access_tuple.storage_keys) for access_tuple in tx.access_list + ]), + ) + sedes = List([schema for schema, _ in schema]) + values = [value for _, value in schema] + + return keccak([0x02] + rlp.encode(values, sedes)) + + assert tx.max_priority_fee_per_gas == tx.max_fee_per_gas + + if hash_version == TX_HASH_VERSION_EIP2930: + schema = ( + (big_endian_int, tx.chain_id), + (big_endian_int, tx.nonce), + (big_endian_int, tx.max_fee_per_gas), + (big_endian_int, tx.gas_limit), + (binary, tx.to if tx.to is not None else []), + (big_endian_int, tx.value), + (binary, tx.data), + (List([Binary[20, 20], List([Binary[32, 32]])]), [ + (access_tuple.address, access_tuple.storage_keys) for access_tuple in tx.access_list + ]), + ) + sedes = List([schema for schema, _ in schema]) + values = [value for _, value in schema] + + return keccak([0x01] + rlp.encode(values, sedes)) + + assert len(tx.access_list) == 0 + + if hash_version == TX_HASH_VERSION_EIP155: + schema = ( + (big_endian_int, tx.nonce), + (big_endian_int, tx.max_fee_per_gas), + (big_endian_int, tx.gas_limit), + (binary, tx.to if tx.to is not None else []), + (big_endian_int, tx.value), + (binary, tx.data), + (big_endian_int, tx.chain_id), + (big_endian_int, 0), + (big_endian_int, 0), + ) + sedes = List([schema for schema, _ in schema]) + values = [value for _, value in schema] + + return keccak(rlp.encode(values, sedes)) + + assert tx.chain_id == 0 + + if hash_version == TX_HASH_VERSION_LEGACY: + schema = ( + (big_endian_int, tx.nonce), + (big_endian_int, tx.max_fee_per_gas), + (big_endian_int, tx.gas_limit), + (binary, tx.to if tx.to is not None else []), + (big_endian_int, tx.value), + (binary, tx.data), + ) + sedes = List([schema for schema, _ in schema]) + values = [value for _, value in schema] + + return keccak(rlp.encode(values, sedes)) + + assert False +``` + +```python +def get_transaction_id(signed_tx: SignedTransaction) -> bytes: + hash_version = signed_tx.hash_version + + if hash_version == TX_HASH_VERSION_EIP4844: + return hash_tree_root(signed_tx) + + assert len(tx.blob_versioned_hashes) == 0 + + if hash_version == TX_HASH_VERSION_EIP1559: + schema = ( + (big_endian_int, tx.chain_id), + (big_endian_int, tx.nonce), + (big_endian_int, tx.max_priority_fee_per_gas), + (big_endian_int, tx.max_fee_per_gas), + (big_endian_int, tx.gas_limit), + (binary, tx.to if tx.to is not None else []), + (big_endian_int, tx.value), + (binary, tx.data), + (List([Binary[20, 20], List([Binary[32, 32]])]), [ + (access_tuple.address, access_tuple.storage_keys) for access_tuple in tx.access_list + ]), + (big_endian_int, 1 if signed_tx.signature.y_parity else 0), + (big_endian_int, signed_tx.signature.r), + (big_endian_int, signed_tx.signature.s), + ) + sedes = List([schema for schema, _ in schema]) + values = [value for _, value in schema] + + return keccak([0x02] + rlp.encode(values, sedes)) + + assert tx.max_priority_fee_per_gas == tx.max_fee_per_gas + + if hash_version == TX_HASH_VERSION_EIP2930: + schema = ( + (big_endian_int, tx.chain_id), + (big_endian_int, tx.nonce), + (big_endian_int, tx.max_fee_per_gas), + (big_endian_int, tx.gas_limit), + (binary, tx.to if tx.to is not None else []), + (big_endian_int, tx.value), + (binary, tx.data), + (List([Binary[20, 20], List([Binary[32, 32]])]), [ + (access_tuple.address, access_tuple.storage_keys) for access_tuple in tx.access_list + ]), + (big_endian_int, 1 if signed_tx.signature.y_parity else 0), + (big_endian_int, signed_tx.signature.r), + (big_endian_int, signed_tx.signature.s), + ) + sedes = List([schema for schema, _ in schema]) + values = [value for _, value in schema] + + return keccak([0x01] + rlp.encode(values, sedes)) + + assert len(tx.access_list) == 0 + + if hash_version == TX_HASH_VERSION_EIP155: + schema = ( + (big_endian_int, tx.nonce), + (big_endian_int, tx.max_fee_per_gas), + (big_endian_int, tx.gas_limit), + (binary, tx.to if tx.to is not None else []), + (big_endian_int, tx.value), + (binary, tx.data), + (big_endian_int, (1 if signed_tx.signature.y_parity else 0) + tx.chain_id * 2 + 35), + (big_endian_int, signed_tx.signature.r), + (big_endian_int, signed_tx.signature.s), + ) + sedes = List([schema for schema, _ in schema]) + values = [value for _, value in schema] + + return keccak(rlp.encode(values, sedes)) + + assert tx.chain_id == 0 + + if hash_version == TX_HASH_VERSION_LEGACY: + schema = ( + (big_endian_int, tx.nonce), + (big_endian_int, tx.max_fee_per_gas), + (big_endian_int, tx.gas_limit), + (binary, tx.to if tx.to is not None else []), + (big_endian_int, tx.value), + (binary, tx.data), + (big_endian_int, (1 if signed_tx.signature.y_parity else 0) + 27), + (big_endian_int, signed_tx.signature.r), + (big_endian_int, signed_tx.signature.s), + ) + sedes = List([schema for schema, _ in schema]) + values = [value for _, value in schema] + + return keccak(rlp.encode(values, sedes)) + + assert False +``` + +```python +def get_transaction_signer(signed_tx: SignedTransaction) -> Address: + if hash_version >= TX_HASH_VERSION_EIP2930: + return ecrecover( + get_transaction_sighash(signed_tx.tx, signed_tx.hash_version), + 1 if signed_tx.signature.y_parity else 0, + signed_tx.signature.r, + signed_tx.signature.s, + ) + + if hash_version >= TX_HASH_VERSION_EIP155: + return ecrecover( + get_transaction_sighash(signed_tx.tx, signed_tx.hash_version), + (1 if signed_tx.signature.y_parity else 0) + tx.chain_id * 2 + 35, + signed_tx.signature.r, + signed_tx.signature.s, + ) + + if hash_version >= TX_HASH_VERSION_LEGACY: + return ecrecover( + get_transaction_sighash(signed_tx.tx, signed_tx.hash_version), + (1 if signed_tx.signature.y_parity else 0) + 27, + signed_tx.signature.r, + signed_tx.signature.s, + ) + + assert False +``` + +```python +def validate_transaction_payload(payload: TransactionPayload): + if payload.signed_tx.hash_version < TX_HASH_VERSION_EIP4844: + assert len(payload.signed_tx.tx.blob_versioned_hashes) == 0 + + if payload.signed_tx.hash_version < TX_HASH_VERSION_EIP1559: + assert payload.signed_tx.tx.max_priority_fee_per_gas == payload.signed_tx.tx.max_fee_per_gas + + if payload.signed_tx.hash_version < TX_HASH_VERSION_EIP2930: + assert len(payload.signed_tx.tx.access_list) == 0 + + if payload.signed_tx.hash_version < TX_HASH_VERSION_EIP155: + assert payload.signed_tx.tx.chain_id == 0 + + if len(payload.signed_tx.tx.blob_versioned_hashes) == 0: + assert payload.blob is None + else: + assert payload.blob is not None + + # Based on EIP-4844 logic + versioned_hashes = payload.signed_tx.tx.blob_versioned_hashes + commitments = payload.blob.blob_kzgs + blobs = payload.blob.blobs + # note: assert blobs are not malformatted + assert len(versioned_hashes) == len(commitments) == len(blobs) + + # Verify that commitments match the blobs by checking the KZG proof + assert verify_aggregate_kzg_proof(blobs, commitments, payload.blob.kzg_aggregated_proof) + + # Now that all commitments have been verified, check that versioned_hashes matches the commitments + for versioned_hash, commitment in zip(versioned_hashes, commitments): + assert versioned_hash == kzg_to_versioned_hash(commitment) +``` + +##### Upgrading transactions to SSZ + +```python +def upgrade_signed_rlp_transaction_to_ssz(rlp_encoded_tx: bytes) -> SignedTransaction: + tx_type = rlp_encoded_tx[0] # EIP-2718 transaction type + + if tx_type == 0x02: + class SignedEIP1559Transaction(rlp.Serializable): + fields = ( + ('chain_id', big_endian_int), + ('nonce', big_endian_int), + ('max_priority_fee_per_gas', big_endian_int), + ('max_fee_per_gas', big_endian_int), + ('gas_limit', big_endian_int), + ('destination', binary), + ('amount', big_endian_int), + ('data', binary), + ('access_list', List([Binary[20, 20], List([Binary[32, 32]])])), + ('signature_y_parity', big_endian_int), + ('signature_r', big_endian_int), + ('signature_s', big_endian_int), + ) + pre = SignedEIP1559Transaction.deserialize(rlp_encoded_tx[1:]) + + return SignedTransaction( + tx=Transaction( + chain_id=pre.chain_id, + nonce=pre.nonce, + max_priority_fee_per_gas=pre.max_priority_fee_per_gas, + max_fee_per_gas=pre.max_fee_per_gas, + gas_limit=pre.gas_limit, + to=Address(pre.destination) if len(pre.destination) > 0 else None, + value=pre.amount, + data=pre.data, + access_list=[AccessTuple( + address=access_tuple[0], + storage_keys=access_tuple[1], + ) for access_tuple in pre.access_list], + ), + signature=ECDSASignature( + y_parity=pre.signature_y_parity != 0, + r=pre.signature_r, + s=pre.signature_s, + ), + hash_version=TX_HASH_VERSION_EIP1559, + ) + + if tx_type == 0x01: + class SignedEIP2930Transaction(rlp.Serializable): + fields = ( + ('chainId', big_endian_int), + ('nonce', big_endian_int), + ('gasPrice', big_endian_int), + ('gasLimit', big_endian_int), + ('to', binary), + ('value', big_endian_int), + ('data', binary), + ('accessList', List([Binary[20, 20], List([Binary[32, 32]])])), + ('signatureYParity', big_endian_int), + ('signatureR', big_endian_int), + ('signatureS', big_endian_int), + ) + pre = SignedEIP2930Transaction.deserialize(rlp_encoded_tx[1:]) + + return SignedTransaction( + tx=Transaction( + chain_id=pre.chainId, + nonce=pre.nonce, + max_priority_fee_per_gas=pre.gasPrice, + max_fee_per_gas=pre.gasPrice, + gas_limit=pre.gasLimit, + to=Address(pre.to) if len(pre.to) > 0 else None, + value=pre.value, + data=pre.data, + access_list=[AccessTuple( + address=access_tuple[0], + storage_keys=access_tuple[1], + ) for access_tuple in pre.accessList], + ), + signature=ECDSASignature( + y_parity=pre.signatureYParity != 0, + r=pre.signatureR, + s=pre.signatureS, + ), + hash_version=TX_HASH_VERSION_EIP2930, + ) + + if 0xc0 <= tx_type <= 0xfe: + (nonce, gasprice, startgas, to, value, data, chainid, 0, 0) + + class SignedLegacyTransaction(rlp.Serializable): + fields = ( + ('nonce', big_endian_int), + ('gasprice', big_endian_int), + ('startgas', big_endian_int), + ('to', binary), + ('value', big_endian_int), + ('data', binary), + ('v', big_endian_int), + ('r', big_endian_int), + ('s', big_endian_int), + ) + pre = SignedLegacyTransaction.deserialize(rlp_encoded_tx) + + if pre.v not in (27, 28): + return SignedTransaction( + tx=Transaction( + chain_id=(pre.v - 35) >> 1, + nonce=pre.nonce, + max_priority_fee_per_gas=pre.gasprice, + max_fee_per_gas=pre.gasprice, + gas_limit=pre.startgas, + to=Address(pre.to) if len(pre.to) > 0 else None, + value=pre.value, + data=pre.data, + ), + signature=ECDSASignature( + y_parity=((pre.v - 35) & 0x1) != 0, + r=pre.r, + s=pre.s, + ), + hash_version=TX_HASH_VERSION_EIP155, + ) + + return SignedTransaction( + tx=Transaction( + nonce=pre.nonce, + max_priority_fee_per_gas=pre.gasprice, + max_fee_per_gas=pre.gasprice, + gas_limit=pre.startgas, + to=Address(pre.to) if len(pre.to) > 0 else None, + value=pre.value, + data=pre.data, + ), + signature=ECDSASignature( + y_parity=((pre.v - 27) & 0x1) != 0, + r=pre.r, + s=pre.s, + ), + hash_version=TX_HASH_VERSION_LEGACY, + ) + + assert False +``` + +### Receipts + +| Name | SSZ equivalent | +| - | - | +| `Topic` | `Bytes32` | + +| Name | Value | +| - | - | +| [`BYTES_PER_LOGS_BLOOM`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#execution) | `uint64(2**8)` (= 256) | +| `MAX_TOPICS_PER_LOG` | `uint64(2**24)` (= 16,777,216) | +| `MAX_LOG_DATA_SIZE` | `uint64(2**24)` (= 16,777,216) | +| `MAX_LOGS_PER_RECEIPT` | `uint64(2**24)` (= 16,777,216) | +| `MAX_RECEIPTS_PER_PAYLOAD` | `uint64(2**20)` (= 1,048,576) | Maximum amount of receipts allowed in each block | + +```python +class Log(Container): + address: Address + topics: List[Topic, MAX_TOPICS_PER_LOG] + data: ByteVector[MAX_LOG_DATA_SIZE] + +class Receipt(Container): + status: uint256 # EIP-658 + cumulative_transaction_gas_used: uint64 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + logs: List[Log, MAX_LOGS_PER_RECEIPT] +``` + +The `blocks.receipts_root` is updated to a SSZ root. To compute the new `receipts_root`, the list of individual `Receipt` containers is represented as a SSZ `List`. + +```python +block.receipts_root == hash_tree_root(List[Receipt, MAX_RECEIPTS_PER_PAYLOAD]( + receipt_0, + receipt_1, + receipt_2, + ... +)) == hash_tree_root(List[Root, MAX_RECEIPTS_PER_PAYLOAD]( + hash_tree_root(receipt_0), + hash_tree_root(receipt_1), + hash_tree_root(receipt_2), + ... +)) +``` + +#### Consensus `ExecutionPayload` changes (Receipts) + +The [CL `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) and [CL `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader) are updated to take account the new CL `receipts_root` type. + +```python +class ExecutionPayload(Container): + ... + receipts_root: Root # Formerly, `Bytes32` + ... + + +class ExecutionPayloadHeader(Container): + ... + receipts_root: Root # Formerly, `Bytes32` + ... +``` + +#### Execution block header changes (Receipts) + +The execution block header's `receipts_root` is updated to match the execution block's `receipts_root` definition. + +```python +block_header.receipts_root = block.receipts_root +``` + +### Withdrawals + +#### Execution block changes (Withdrawals) + +The individual [`Withdrawal` objects](./eip-4895.md) within the execution block's `withdrawals` list are now represented as SSZ `Container`s, matching the definition of the [consensus-specs `Withdrawal`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/capella/beacon-chain.md#withdrawal). + +```python +class Withdrawal(Container): + index: WithdrawalIndex + validator_index: ValidatorIndex + address: ExecutionAddress + amount: Gwei +``` + +The `blocks.withdrawals` list remains RLP encoded: + +```python +block.withdrawals = RLP([ + SSZ.encode(withdrawal_0), + SSZ.encode(withdrawal_1), + SSZ.encode(withdrawal_2), + ... +]) +``` + +#### Execution block header changes (Withdrawals) + +The execution block header's `withdrawals_root` is updated to the `withdrawals_root` definition of the [consensus-specs `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader). + +To compute the new `withdrawals_root`, the list of individual `Withdrawal` containers is represented as a SSZ `List`. + +| Name | Value | Description | +| - | - | - | +| [`MAX_WITHDRAWALS_PER_PAYLOAD`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/capella/beacon-chain.md#execution) | `uint64(2**4)` (= 16) | Maximum amount of withdrawals allowed in each block | + +```python +block_header.withdrawals_root == hash_tree_root(List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD]( + withdrawal_0, + withdrawal_1, + withdrawal_2, + ... +)) == hash_tree_root(List[Root, MAX_WITHDRAWALS_PER_PAYLOAD]( + hash_tree_root(withdrawal_0), + hash_tree_root(withdrawal_1), + hash_tree_root(withdrawal_2), + ... +)) +``` + +### Deposits + +Same as withdrawals, but ideally we have SSZ by the time deposits arrive. + +Also, if more MPT are introduced by other EIP, same story. + +### Block structure itself (TBD) + +Converting the execution block and the execution block header to SSZ would align their `hash_tree_root` value with `hash_tree_root(ExecutionPayload)` and `hash_tree_root(ExecutionPayloadHeader)` on the CL. This would allow forming end-to-end SSZ proofs from any execution data (except data rooted in `state_root`). Such proofs could share a generic SSZ multiproof API format across consensus and execution layers. + +- The CL `ExecutionPayloadHeader` would become equivalent to the EL block header. The `block_hash` field in the `ExecutionPayloadHeader` can be removed, shortening the merkle proof for applications interested in just the execution block hash. + +- The CL `ExecutionPayload` would become a subset of the EL block, with the `receipts` field replaced by `receipts_root`, as `Receipt` structures are only stored by the EL. + +TBD + +## Rationale + +### Why not multiple `Transaction` containers? + +- **Superset of all existing transaction types:** The new `Transaction` container supports all existing transaction types. There is no new functionality that was previously disallowed. `Transaction` containers that are created from importing legacy transaction types use default values for fields that were added later. + +- **Static merkle tree shape:** Compared to approaches based on SSZ `Union`, it is not necessary to branch on `hash_version` to determine the `GeneralizedIndex` for common fields. For example, a proof for a `Transaction`'s `value` field always has the exact same structure. + +- **Shorter merkle proofs:** Compared to approaches based on SSZ `Onion` (merkleized as \[o]bject, serialized as u\[nion]), there is less nesting in the merkle tree. This makes proofs shorter (32 bytes per nesting level) + +- **Easy to deprecate legacy transactions:** If a transaction type is no longer allowed, the tree shape remains static. In non-archive settings, the `upgrade_signed_rlp_transaction_to_ssz`, `get_transaction_id` and `get_transaction_sighash` functions can be adjusted to drop support for legacy transaction types. + +- **Discourage legacy transaction types:** There are no block size benefits to use a legacy transaction type anymore. A penalty fee could be charged to new legacy transactions to promote timely upgrade to latest `Transaction` format. + +- **Prior art:** Multiple modules of Ethereum already process common fields in a unified way. The CL pytests use `is_post_fork` to conditionally enable logic. The execution JSON-RPC reports transaction fields under the same name regardless of type. The CL light client protocol incorporates a very similar mechanism for upgrading CL `ExecutionPayloadHeader` to later formats: [`get_transaction_sighash` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/sync-protocol.md#modified-get_lc_execution_root) / [`upgrade_to_latest` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/full-node.md#modified-block_to_light_client_header). + +## Backwards Compatibility + +Applications that solely rely on the legacy RLP encoding but do not rely on the MPT commitment in the block header can still be used through a re-encoding proxy. + +Applications that rely on the replaced MPT root commitments in the block header can no longer find that information. Analysis is required whether affected applications have a migration path available. If major applications can't migrate, can keep the legacy MPT commitment in the header for now, and introduce the new SSZ trees next to them. + +- **Transactions:** The txid is commonly used by block explorers. A helper function `get_transaction_id` is specified to replicate historic transaction IDs. + +- **Receipts:** The concept of a "receipt ID" is not commonly used. There is also no JSON-RPC endpoint to obtain a receipts proof. It is not expected that major applications rely on the legacy MPT for receipts. + +- **Withdrawals:** Withdrawals were introduced in Capella/Shanghai. It is not expected that major applications rely on the legacy MPT for withdrawals. + +## Test Cases + +### SSZ tests (Transactions) + +```python +# Without EIP-4844 blobs +SSZ.encode(TransactionPayload( + signed_tx=SignedTransaction( + tx=Transaction( + chain_id=uint64(0x07), + nonce=uint64(0x0a), + max_priority_fee_per_gas=uint256(0x0f), + max_fee_per_gas=uint256(0x04), + gas_limit=uint64(0x05), + to=fromhex("0000000000000000000000000000000000000008"), + value=uint256(0x06), + data=[0x09], + access_list=[ + AccessTuple( + address=fromhex("000000000000000000000000000000000000000b"), + storage_keys=[ + fromhex("000000000000000000000000000000000000000000000000000000000000000c"), + fromhex("000000000000000000000000000000000000000000000000000000000000000d"), + ], + ), + AccessTuple( + address=fromhex("000000000000000000000000000000000000000e"), + ), + ], + ), + signature=ECDSASignature( + y_parity=true, + r=uint256(0x02), + s=uint256(0x03), + ), + hash_version=TxHashVersion(0x04), + ), +)) == fromhex( + # offset(payload.signed_tx) + "08000000" + + # offset(payload.blob) (unused) + "63010000" + + # offset(signed_tx.tx) + "46000000" + + # signed_tx.signature + "01" + + "0200000000000000000000000000000000000000000000000000000000000000" + + "0300000000000000000000000000000000000000000000000000000000000000" + + # signed_tx.hash_version + "04" + + # tx.chain_id + "0700000000000000" + + # tx.nonce + "0a00000000000000" + + # tx.max_priority_fee_per_gas + "0f00000000000000000000000000000000000000000000000000000000000000" + + # tx.max_fee_per_gas + "0400000000000000000000000000000000000000000000000000000000000000" + + # tx.gas_limit + "0500000000000000" + + # offset(tx.to) + "88000000" + + # tx.value + "0600000000000000000000000000000000000000000000000000000000000000" + + # offset(tx.data) + "9c000000" + + # offset(tx.access_list) + "9d000000" + + # offset(tx.blob_versioned_hashes) (unused) + "15010000" + + # tx.to + "0000000000000000000000000000000000000008" + + # tx.data + "09" + + # offset(tx.access_tuple[0]) + "08000000" + + # offset(tx.access_tuple[1]) + "60000000" + + # access_tuple[0].address + "000000000000000000000000000000000000000b" + + # offset(access_tuple[0].storage_keys) + "18000000" + + # access_tuple[0].storage_keys[0] + "000000000000000000000000000000000000000000000000000000000000000c" + + # access_tuple[0].storage_keys[1] + "000000000000000000000000000000000000000000000000000000000000000d" + + # access_tuple[1].address + "000000000000000000000000000000000000000e" + + # offset(access_tuple[1].storage_keys) + "18000000" +) + +# With EIP-4844 blobs +SSZ.encode(TransactionPayload( + signed_tx=SignedTransaction( + tx=Transaction( + chain_id=uint64(0x07), + nonce=uint64(0x0a), + max_priority_fee_per_gas=uint256(0x0f), + max_fee_per_gas=uint256(0x04), + gas_limit=uint64(0x05), + to=fromhex("0000000000000000000000000000000000000008"), + value=uint256(0x06), + data=[0x09], + access_list=[ + AccessTuple( + address=fromhex("000000000000000000000000000000000000000b"), + storage_keys=[ + fromhex("000000000000000000000000000000000000000000000000000000000000000c"), + fromhex("000000000000000000000000000000000000000000000000000000000000000d"), + ], + ), + AccessTuple( + address=fromhex("000000000000000000000000000000000000000e"), + ), + ], + blob_versioned_hashes=[ + fromhex("0000000000000000000000000000000000000000000000000000000000000010"), + ], + ), + signature=ECDSASignature( + y_parity=true, + r=uint256(0x02), + s=uint256(0x03), + ), + hash_version=TxHashVersion(0x04), + ), + blob=BlobTransactionExtension( + blob_kzgs=[ + [0x11] + [0x00] * 47, + ], + blobs=[ + [[uint256(0x12)] + [uint256(0x00)] * 4_095], + ], + kzg_aggregated_proof=[0x13] + [0x00] * 47, + ) +)) == fromhex( + # offset(payload.signed_tx) + "08000000" + + # offset(payload.blob) + "83010000" + + # offset(signed_tx.tx) + "46000000" + + # signed_tx.signature + "01" + + "0200000000000000000000000000000000000000000000000000000000000000" + + "0300000000000000000000000000000000000000000000000000000000000000" + + # signed_tx.hash_version + "04" + + # tx.chain_id + "0700000000000000" + + # tx.nonce + "0a00000000000000" + + # tx.max_priority_fee_per_gas + "0f00000000000000000000000000000000000000000000000000000000000000" + + # tx.max_fee_per_gas + "0400000000000000000000000000000000000000000000000000000000000000" + + # tx.gas_limit + "0500000000000000" + + # offset(tx.to) + "88000000" + + # tx.value + "0600000000000000000000000000000000000000000000000000000000000000" + + # offset(tx.data) + "9c000000" + + # offset(tx.access_list) + "9d000000" + + # offset(tx.blob_versioned_hashes) + "15010000" + + # tx.to + "0000000000000000000000000000000000000008" + + # tx.data + "09" + + # offset(tx.access_tuple[0]) + "08000000" + + # offset(tx.access_tuple[1]) + "60000000" + + # access_tuple[0].address + "000000000000000000000000000000000000000b" + + # offset(access_tuple[0].storage_keys) + "18000000" + + # access_tuple[0].storage_keys[0] + "000000000000000000000000000000000000000000000000000000000000000c" + + # access_tuple[0].storage_keys[1] + "000000000000000000000000000000000000000000000000000000000000000d" + + # access_tuple[1].address + "000000000000000000000000000000000000000e" + + # offset(access_tuple[1].storage_keys) + "18000000" + + # blob_versioned_hashes[0] + "0000000000000000000000000000000000000000000000000000000000000010" + + # offset(blob.blob_kzgs) + "38000000" + + # offset(blob.blobs) + "68000000" + + # blob.kzg_aggregated_proof + "130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + # blob.blob_kzgs[0] + "110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + # blob.blobs[0] + "1200000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + ... + "0000000000000000000000000000000000000000000000000000000000000000" +) +``` + +### SSZ tests (Receipts) + +TBD + +### SSZ tests (Withdrawals) + +```python +withdrawal = Withdrawal( + index=WithdrawalIndex(0x0123), + validator_index=ValidatorIndex(0x4567), + address=fromhex("0001020304050607080910111213141516171819"), + amount=Gwei(0x89ab), +) + +SSZ.encode(withdrawal) == fromhex( + "2301000000000000" + + "6745000000000000" + + "0001020304050607080910111213141516171819" + + "ab89000000000000" +) + +hash_tree_root(withdrawal) == sha256( + sha256(fromhex( + "2301000000000000000000000000000000000000000000000000000000000000" + + "6745000000000000000000000000000000000000000000000000000000000000")) + + sha256(fromhex( + "0001020304050607080910111213141516171819000000000000000000000000" + + "ab89000000000000000000000000000000000000000000000000000000000000")) +) == fromhex("af8247942122ba68d69d2784dfd9994fa16681ff6feb2680d7922c61a899a20d") +``` + +```python +withdrawals = List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD](withdrawal_0, withdrawal_1, withdrawal_2) + +block_header.withdrawals_root == hash_tree_root(withdrawals) == sha256( + sha256( + sha256( + sha256( + sha256(hash_tree_root(withdrawal_0) + hash_tree_root(withdrawal_1)) + + sha256(hash_tree_root(withdrawal_2) + hash_tree_root(Withdrawal()))) + + sha256( + sha256(hash_tree_root(Withdrawal()) + hash_tree_root(Withdrawal())) + + sha256(hash_tree_root(Withdrawal()) + hash_tree_root(Withdrawal())))) + + sha256( + sha256( + sha256(hash_tree_root(Withdrawal()) + hash_tree_root(Withdrawal())) + + sha256(hash_tree_root(Withdrawal()) + hash_tree_root(Withdrawal()))) + + sha256( + sha256(hash_tree_root(Withdrawal()) + hash_tree_root(Withdrawal())) + + sha256(hash_tree_root(Withdrawal()) + hash_tree_root(Withdrawal()))))) + + fromhex("0300000000000000000000000000000000000000000000000000000000000000") # len +) +``` + +## Reference Implementation + +TBD + +## Security Considerations + +Transaction hashes are keccak256, but SSZ roots are SHA256. We could end up in a situation where we would like to mix them in same database table, etc. Hash functions are normally only collision resistant with itself. Cryptoanalysis is required to determine whether keccak256 and SHA256 are safe to mix. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From 1676478ea6ea404f7fdc27af8461f1f1d1ced92e Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Thu, 2 Feb 2023 16:34:21 +0100 Subject: [PATCH 02/25] Avoid CL/EL abbrev --- EIPS/eip-6404.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 2aafa3b6406caf..6922cca891f226 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -1,7 +1,7 @@ --- eip: 6404 title: Transition to SSZ -description: Unifies the CL and EL block representations +description: Unifies block representation across execution and consensus author: Etan Kissling (@etan-status) discussions-to: https://ethereum-magicians.org/t/eip-6404-transition-to-ssz/12783 status: Draft @@ -13,19 +13,19 @@ requires: 155, 658, 1559, 2718, 2930, 4844, 4895 ## Abstract -This EIP aligns the definition of the EL block with the CL `ExecutionPayload` structure. +This EIP aligns the definition of the execution block with the consensus `ExecutionPayload` structure. ## Motivation -While the CL `ExecutionPayload` and the EL block structure map to each other, they are differently encoded. This EIP aims to align the encoding between them, taking advantage of the more modern SSZ format used by the CL. This brings several advantages: +While the consensus `ExecutionPayload` and the execution block structure map to each other, they are encoded differently. This EIP aims to align the encoding between them, taking advantage of the more modern SSZ format. This brings several advantages: 1. **Reducing complexity:** Merkle-Patricia Tries (MPT) are hard to work with. Replacing them with SSZ leaves only the state trie in the legacy MPT format. 2. **Better for smart contracts:** The SSZ format is optimized for production and verification of merkle proofs. It allows proving specific fields of containers and allows chunked processing, e.g., to support handling transactions that do not fit into calldata. -3. **Better for light clients:** Light clients with access to the CL `ExecutionPayload` no longer need to obtain the matching EL block header to verify proofs rooted in `transactions_root` or `withdrawals_root`. +3. **Better for light clients:** Light clients with access to the consensus `ExecutionPayload` no longer need to obtain the matching execution block header to verify proofs rooted in `transactions_root` or `withdrawals_root`. -4. **Reducing ambiguity:** The names `transactions_root` and `withdrawals_root` are currently used by both EL and CL to refer to different roots. The EL refers to a MPT root, the CL refers to a SSZ root. +4. **Reducing ambiguity:** The names `transactions_root` and `withdrawals_root` are currently used to refer to different roots. The execution block header refers to a MPT root, the consensus `ExecutionPayloadHeader` refers to a SSZ root. ## Specification @@ -142,7 +142,7 @@ block.transactions = RLP([ #### Consensus `ExecutionPayload` changes (Transactions) -The [CL `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) is updated to replace the existing [CL `Transaction`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#custom-types) container with the new `SignedTransaction` container. +The [consensus `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) is updated to replace the existing [consensus `Transaction`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#custom-types) container with the new `SignedTransaction` container. | Name | Value | Description | | - | - | - | @@ -609,7 +609,7 @@ block.receipts_root == hash_tree_root(List[Receipt, MAX_RECEIPTS_PER_PAYLOAD]( #### Consensus `ExecutionPayload` changes (Receipts) -The [CL `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) and [CL `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader) are updated to take account the new CL `receipts_root` type. +The [consensus `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) and [consensus `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader) are updated to take account the new consensus `receipts_root` type. ```python class ExecutionPayload(Container): @@ -689,11 +689,11 @@ Also, if more MPT are introduced by other EIP, same story. ### Block structure itself (TBD) -Converting the execution block and the execution block header to SSZ would align their `hash_tree_root` value with `hash_tree_root(ExecutionPayload)` and `hash_tree_root(ExecutionPayloadHeader)` on the CL. This would allow forming end-to-end SSZ proofs from any execution data (except data rooted in `state_root`). Such proofs could share a generic SSZ multiproof API format across consensus and execution layers. +Converting the execution block and the execution block header to SSZ would align their `hash_tree_root` value with the consensus `hash_tree_root(ExecutionPayload)` and `hash_tree_root(ExecutionPayloadHeader)`. This would allow forming end-to-end SSZ proofs from any execution data (except data rooted in `state_root`). Such proofs could share a generic SSZ multiproof API format across consensus and execution layers. -- The CL `ExecutionPayloadHeader` would become equivalent to the EL block header. The `block_hash` field in the `ExecutionPayloadHeader` can be removed, shortening the merkle proof for applications interested in just the execution block hash. +- The consensus `ExecutionPayloadHeader` would become equivalent to the execution block header. The `block_hash` field in the `ExecutionPayloadHeader` can be removed, shortening the merkle proof for applications interested in just the execution block hash. -- The CL `ExecutionPayload` would become a subset of the EL block, with the `receipts` field replaced by `receipts_root`, as `Receipt` structures are only stored by the EL. +- The consensus `ExecutionPayload` would become a subset of the execution block, with the `receipts` field replaced by `receipts_root`, as `Receipt` structures are only stored by the execution. TBD @@ -711,7 +711,7 @@ TBD - **Discourage legacy transaction types:** There are no block size benefits to use a legacy transaction type anymore. A penalty fee could be charged to new legacy transactions to promote timely upgrade to latest `Transaction` format. -- **Prior art:** Multiple modules of Ethereum already process common fields in a unified way. The CL pytests use `is_post_fork` to conditionally enable logic. The execution JSON-RPC reports transaction fields under the same name regardless of type. The CL light client protocol incorporates a very similar mechanism for upgrading CL `ExecutionPayloadHeader` to later formats: [`get_transaction_sighash` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/sync-protocol.md#modified-get_lc_execution_root) / [`upgrade_to_latest` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/full-node.md#modified-block_to_light_client_header). +- **Prior art:** Multiple modules of Ethereum already process common fields in a unified way. The consensus pytests use `is_post_fork` to conditionally enable logic. The execution JSON-RPC reports transaction fields under the same name regardless of type. The consensus light client protocol incorporates a very similar mechanism for upgrading consensus `ExecutionPayloadHeader` to later formats: [`get_transaction_sighash` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/sync-protocol.md#modified-get_lc_execution_root) / [`upgrade_to_latest` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/full-node.md#modified-block_to_light_client_header). ## Backwards Compatibility From 0659b5741feaa90e375fdcf62c3dddd196dd6796 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Thu, 2 Feb 2023 17:04:59 +0100 Subject: [PATCH 03/25] Cleanups from review --- EIPS/eip-6404.md | 59 ++---------------------------------------------- 1 file changed, 2 insertions(+), 57 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 6922cca891f226..d7a6a36742a55d 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -129,17 +129,6 @@ class TransactionPayload(Container): On the network, `TransactionPayload` is preceded by a single byte, [`BLOB_TX_TYPE`](./eip-4844.md#parameters) to distinguish from other transaction formats according to [EIP-2718](./eip-2718.md), regardless of the inner `signed_tx.hash_version`. -The `blocks.transactions` list remains RLP encoded: - -```python -block.transactions = RLP([ - SSZ.encode(transaction_0), - SSZ.encode(transaction_1), - SSZ.encode(transaction_2), - ... -]) -``` - #### Consensus `ExecutionPayload` changes (Transactions) The [consensus `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) is updated to replace the existing [consensus `Transaction`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#custom-types) container with the new `SignedTransaction` container. @@ -157,7 +146,7 @@ class ExecutionPayload(Container): #### Execution block header changes (Transactions) -The execution block header's `transactions_root` is updated to the `transactions_root` definition of the [consensus-specs `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader). +The execution block header's `transactions_root` is updated to match the consensus `ExecutionPayloadHeader`. To compute the new `transactions_root`, the list of individual `SignedTransaction` containers is represented as a SSZ `List`. @@ -167,11 +156,6 @@ block_header.transactions_root == hash_tree_root(List[SignedTransaction, MAX_TRA transaction_1, transaction_2, ... -)) == hash_tree_root(List[Root, MAX_TRANSACTIONS_PER_PAYLOAD]( - hash_tree_root(transaction_0), - hash_tree_root(transaction_1), - hash_tree_root(transaction_2), - ... )) ``` @@ -356,35 +340,6 @@ def get_transaction_id(signed_tx: SignedTransaction) -> bytes: assert False ``` -```python -def get_transaction_signer(signed_tx: SignedTransaction) -> Address: - if hash_version >= TX_HASH_VERSION_EIP2930: - return ecrecover( - get_transaction_sighash(signed_tx.tx, signed_tx.hash_version), - 1 if signed_tx.signature.y_parity else 0, - signed_tx.signature.r, - signed_tx.signature.s, - ) - - if hash_version >= TX_HASH_VERSION_EIP155: - return ecrecover( - get_transaction_sighash(signed_tx.tx, signed_tx.hash_version), - (1 if signed_tx.signature.y_parity else 0) + tx.chain_id * 2 + 35, - signed_tx.signature.r, - signed_tx.signature.s, - ) - - if hash_version >= TX_HASH_VERSION_LEGACY: - return ecrecover( - get_transaction_sighash(signed_tx.tx, signed_tx.hash_version), - (1 if signed_tx.signature.y_parity else 0) + 27, - signed_tx.signature.r, - signed_tx.signature.s, - ) - - assert False -``` - ```python def validate_transaction_payload(payload: TransactionPayload): if payload.signed_tx.hash_version < TX_HASH_VERSION_EIP4844: @@ -599,11 +554,6 @@ block.receipts_root == hash_tree_root(List[Receipt, MAX_RECEIPTS_PER_PAYLOAD]( receipt_1, receipt_2, ... -)) == hash_tree_root(List[Root, MAX_RECEIPTS_PER_PAYLOAD]( - hash_tree_root(receipt_0), - hash_tree_root(receipt_1), - hash_tree_root(receipt_2), - ... )) ``` @@ -626,7 +576,7 @@ class ExecutionPayloadHeader(Container): #### Execution block header changes (Receipts) -The execution block header's `receipts_root` is updated to match the execution block's `receipts_root` definition. +The execution block header's `receipts_root` is updated to match the consensus `ExecutionPayloadHeader`. ```python block_header.receipts_root = block.receipts_root @@ -673,11 +623,6 @@ block_header.withdrawals_root == hash_tree_root(List[Withdrawal, MAX_WITHDRAWALS withdrawal_1, withdrawal_2, ... -)) == hash_tree_root(List[Root, MAX_WITHDRAWALS_PER_PAYLOAD]( - hash_tree_root(withdrawal_0), - hash_tree_root(withdrawal_1), - hash_tree_root(withdrawal_2), - ... )) ``` From a2debb20195d3ed62b43bea8ceb6292c33c96146 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Thu, 2 Feb 2023 17:16:35 +0100 Subject: [PATCH 04/25] remove block header conversion from scope --- EIPS/eip-6404.md | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index d7a6a36742a55d..9a1000e54dede3 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -1,7 +1,7 @@ --- eip: 6404 -title: Transition to SSZ -description: Unifies block representation across execution and consensus +title: SSZ transactions, receipts, and withdrawals +description: Migrates MPT commitments for transactions, receipts, and withdrawals to SSZ author: Etan Kissling (@etan-status) discussions-to: https://ethereum-magicians.org/t/eip-6404-transition-to-ssz/12783 status: Draft @@ -17,7 +17,7 @@ This EIP aligns the definition of the execution block with the consensus `Execut ## Motivation -While the consensus `ExecutionPayload` and the execution block structure map to each other, they are encoded differently. This EIP aims to align the encoding between them, taking advantage of the more modern SSZ format. This brings several advantages: +While the consensus `ExecutionPayload` and the execution block structure map to each other conceptually, they are encoded differently. This EIP aims to align the encoding of their fields, taking advantage of the more modern SSZ format. This brings several advantages: 1. **Reducing complexity:** Merkle-Patricia Tries (MPT) are hard to work with. Replacing them with SSZ leaves only the state trie in the legacy MPT format. @@ -632,16 +632,6 @@ Same as withdrawals, but ideally we have SSZ by the time deposits arrive. Also, if more MPT are introduced by other EIP, same story. -### Block structure itself (TBD) - -Converting the execution block and the execution block header to SSZ would align their `hash_tree_root` value with the consensus `hash_tree_root(ExecutionPayload)` and `hash_tree_root(ExecutionPayloadHeader)`. This would allow forming end-to-end SSZ proofs from any execution data (except data rooted in `state_root`). Such proofs could share a generic SSZ multiproof API format across consensus and execution layers. - -- The consensus `ExecutionPayloadHeader` would become equivalent to the execution block header. The `block_hash` field in the `ExecutionPayloadHeader` can be removed, shortening the merkle proof for applications interested in just the execution block hash. - -- The consensus `ExecutionPayload` would become a subset of the execution block, with the `receipts` field replaced by `receipts_root`, as `Receipt` structures are only stored by the execution. - -TBD - ## Rationale ### Why not multiple `Transaction` containers? From 7ed04b00b3eb6ef10a3466da4d6da9b734e7fa55 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Thu, 2 Feb 2023 17:16:45 +0100 Subject: [PATCH 05/25] remove security discussion from scope --- EIPS/eip-6404.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 9a1000e54dede3..b39f4331e1f03f 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -164,7 +164,7 @@ block_header.transactions_root == hash_tree_root(List[SignedTransaction, MAX_TRA ```python def get_transaction_sighash(tx: Transaction, hash_version: TxHashVersion) -> bytes: if hash_version == TX_HASH_VERSION_EIP4844: - return hash_tree_root(tx) + return keccak([0x05] + hash_tree_root(tx)) assert len(tx.blob_versioned_hashes) == 0 @@ -250,7 +250,7 @@ def get_transaction_id(signed_tx: SignedTransaction) -> bytes: hash_version = signed_tx.hash_version if hash_version == TX_HASH_VERSION_EIP4844: - return hash_tree_root(signed_tx) + return keccak([0x05] + hash_tree_root(signed_tx)) assert len(tx.blob_versioned_hashes) == 0 @@ -928,7 +928,7 @@ TBD ## Security Considerations -Transaction hashes are keccak256, but SSZ roots are SHA256. We could end up in a situation where we would like to mix them in same database table, etc. Hash functions are normally only collision resistant with itself. Cryptoanalysis is required to determine whether keccak256 and SHA256 are safe to mix. +None ## Copyright From 853421117627487dbd9220c063db422da0e51b05 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Thu, 2 Feb 2023 17:18:19 +0100 Subject: [PATCH 06/25] Update discussion link for new title --- EIPS/eip-6404.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index b39f4331e1f03f..7f2988ee3ee485 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -3,7 +3,7 @@ eip: 6404 title: SSZ transactions, receipts, and withdrawals description: Migrates MPT commitments for transactions, receipts, and withdrawals to SSZ author: Etan Kissling (@etan-status) -discussions-to: https://ethereum-magicians.org/t/eip-6404-transition-to-ssz/12783 +discussions-to: https://ethereum-magicians.org/t/eip-6404-ssz-transactions-receipts-and-withdrawals/12783 status: Draft type: Standards Track category: Core From 24bdf2c126f81c66985d011a8020848e31dc109d Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Thu, 2 Feb 2023 17:20:07 +0100 Subject: [PATCH 07/25] Clarifying comment about why receipts don't have LC issue --- EIPS/eip-6404.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 7f2988ee3ee485..a09e52517183e5 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -23,7 +23,7 @@ While the consensus `ExecutionPayload` and the execution block structure map to 2. **Better for smart contracts:** The SSZ format is optimized for production and verification of merkle proofs. It allows proving specific fields of containers and allows chunked processing, e.g., to support handling transactions that do not fit into calldata. -3. **Better for light clients:** Light clients with access to the consensus `ExecutionPayload` no longer need to obtain the matching execution block header to verify proofs rooted in `transactions_root` or `withdrawals_root`. +3. **Better for light clients:** Light clients with access to the consensus `ExecutionPayload` no longer need to obtain the matching execution block header to verify proofs rooted in `transactions_root` or `withdrawals_root`. Note that the `receipts_root` is currently represented as a MPT root across both execution and consensus. 4. **Reducing ambiguity:** The names `transactions_root` and `withdrawals_root` are currently used to refer to different roots. The execution block header refers to a MPT root, the consensus `ExecutionPayloadHeader` refers to a SSZ root. From e57a5390d16b1af6f9dd8002d511da088816d4db Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Thu, 2 Feb 2023 21:57:48 +0100 Subject: [PATCH 08/25] Update description --- EIPS/eip-6404.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index a09e52517183e5..6fda7c1502788d 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -1,7 +1,7 @@ --- eip: 6404 title: SSZ transactions, receipts, and withdrawals -description: Migrates MPT commitments for transactions, receipts, and withdrawals to SSZ +description: Migration of MPT commitments for transactions, receipts, and withdrawals to SSZ author: Etan Kissling (@etan-status) discussions-to: https://ethereum-magicians.org/t/eip-6404-ssz-transactions-receipts-and-withdrawals/12783 status: Draft @@ -13,7 +13,7 @@ requires: 155, 658, 1559, 2718, 2930, 4844, 4895 ## Abstract -This EIP aligns the definition of the execution block with the consensus `ExecutionPayload` structure. +This EIP defines a migration process of existing Merkle-Patricia Trie (MPT) commitments for transactions, receipts, and withdrawals to SSZ. ## Motivation From 0881b15e63515ebbf220a77fa2685f451deb2b58 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Thu, 2 Feb 2023 21:59:53 +0100 Subject: [PATCH 09/25] Remove blob wrapper; Sync EIP-2718 nums; Helpers for non-tx; data-gas --- EIPS/eip-6404.md | 473 +++++++++++++++++++++++------------------------ 1 file changed, 228 insertions(+), 245 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 6fda7c1502788d..47e6959ec80584 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -63,32 +63,30 @@ For the purpose of transaction signature validation, it is necessary to recover | Name | Value | Description | | - | - | - | -| `TX_HASH_VERSION_LEGACY` | `TxHashVersion(0x00)` | Replayable [`LegacyTransaction`](./eip-2718.md#transactions) | -| `TX_HASH_VERSION_EIP155` | `TxHashVersion(0x01)` | [EIP-155](./eip-155.md) `LegacyTransaction` | -| `TX_HASH_VERSION_EIP2930` | `TxHashVersion(0x02)` | [EIP-2930](./eip-2930.md#definitions) transaction | -| `TX_HASH_VERSION_EIP1559` | `TxHashVersion(0x03)` | [EIP-1559](./eip-1559.md#specification) transaction | -| `TX_HASH_VERSION_EIP4844` | `TxHashVersion(0x04)` | [EIP-4844](./eip-4844.md#parameters) transaction | +| `TX_HASH_VERSION_LEGACY` | `TxHashVersion(0x03)` | Replayable [`LegacyTransaction`](./eip-2718.md#transactions) | +| `TX_HASH_VERSION_EIP155` | `TxHashVersion(0x04)` | [EIP-155](./eip-155.md) `LegacyTransaction` | +| `TX_HASH_VERSION_EIP2930` | `TxHashVersion(0x01)` | [EIP-2930](./eip-2930.md#definitions) transaction | +| `TX_HASH_VERSION_EIP1559` | `TxHashVersion(0x02)` | [EIP-1559](./eip-1559.md#specification) transaction | +| `TX_HASH_VERSION_EIP4844` | `TxHashVersion(0x05)` | [EIP-4844](./eip-4844.md#parameters) transaction | -#### Execution block changes (Transactions) +#### [EIP-2718](./eip-2718.md) transaction types -A new `Transaction` SSZ container is introduced to represent transactions. +The values `0x00`, `0x03`, and `0x04` are marked as reserved [EIP-2718](./eip-2718.md) transaction types. + +- `0x00` is reserved to avoid ambiguity with `type` in the execution JSON-RPC API, where `0x00` denotes a `LegacyTransaction` with chain ID encoded into the `v` value of the signature according to [EIP-155](./eip-155.md). Transactions of type `0x03` and `0x04` may be represented as `0x00` for backwards compatibility reasons. +- `0x03` and `0x04` are reserved to avoid ambiguity with `TX_HASH_VERSION_LEGACY` and `TX_HASH_VERSION_EIP155`. + +#### Consensus `ExecutionPayload` changes (Transactions) + +The existing [consensus `Transaction`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#custom-types) container represents transactions as opaque, serialized [`EIP-2718`](./eip-2718.md) typed transactions. Once this EIP activates, this definition is replaced with a new `SignedTransaction` SSZ container. | Name | SSZ equivalent | | - | - | -| [`BLSFieldElement`](./eip-4844.md#type-aliases) | `uint256` | | [`VersionedHash`](./eip-4844.md#type-aliases) | `Bytes32` | -| [`KZGCommitment`](./eip-4844.md#type-aliases) | `Bytes48` | -| [`KZGProof`](./eip-4844.md#type-aliases) | `Bytes48` | | Name | Value | | - | - | -| [`FIELD_ELEMENTS_PER_BLOB`](./eip-4844.md#parameters) | `uint64(2**12)` (= 4,096) | | [`MAX_VERSIONED_HASHES_LIST_SIZE`](./eip-4844.md#parameters) | `uint64(2**24)` (= 16,777,216) | -| [`MAX_CALLDATA_SIZE`](./eip-4844.md#parameters) | `uint64(2**24)` (= 16,777,216) | -| [`MAX_ACCESS_LIST_SIZE`](./eip-4844.md#parameters) | `uint64(2**24)` (= 16,777,216) | -| [`MAX_ACCESS_LIST_STORAGE_KEYS`](./eip-4844.md#parameters) | `uint64(2**24)` (= 16,777,216) | -| [`MAX_TX_WRAP_KZG_COMMITMENTS`](./eip-4844.md#parameters) | `uint64(2**24)` (= 16,777,216) | -| [`LIMIT_BLOBS_PER_TX`](./eip-4844.md#parameters) | `uint64(2**24)` (= 16,777,216) | ```python class AccessTuple(Container): @@ -105,6 +103,7 @@ class Transaction(Container): value: uint256 data: ByteList[MAX_CALLDATA_SIZE] access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE] # EIP-2930 + max_fee_per_data_gas: uint256 # EIP-4844 blob_versioned_hashes: List[VersionedHash, MAX_VERSIONED_HASHES_LIST_SIZE] # EIP-4844 class ECDSASignature(Container): @@ -116,22 +115,9 @@ class SignedTransaction(Container): tx: Transaction signature: ECDSASignature hash_version: TxHashVersion - -class BlobTransactionExtension(Container): - blob_kzgs: List[KZGCommitment, MAX_TX_WRAP_KZG_COMMITMENTS] - blobs: List[Vector[BLSFieldElement, FIELD_ELEMENTS_PER_BLOB], LIMIT_BLOBS_PER_TX] - kzg_aggregated_proof: KZGProof - -class TransactionPayload(Container): - signed_tx: SignedTransaction - blob: Optional[BlobTransactionExtension] # EIP-4844 ``` -On the network, `TransactionPayload` is preceded by a single byte, [`BLOB_TX_TYPE`](./eip-4844.md#parameters) to distinguish from other transaction formats according to [EIP-2718](./eip-2718.md), regardless of the inner `signed_tx.hash_version`. - -#### Consensus `ExecutionPayload` changes (Transactions) - -The [consensus `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) is updated to replace the existing [consensus `Transaction`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#custom-types) container with the new `SignedTransaction` container. +The [consensus `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) is updated to use the new `SignedTransaction` SSZ container. | Name | Value | Description | | - | - | - | @@ -140,32 +126,30 @@ The [consensus `ExecutionPayload`](https://github.com/ethereum/consensus-specs/b ```python class ExecutionPayload(Container): ... - transactions: List[SignedTransaction, MAX_TRANSACTIONS_PER_PAYLOAD] # Formerly, `Transaction` + transactions: List[SignedTransaction, MAX_TRANSACTIONS_PER_PAYLOAD] ... ``` -#### Execution block header changes (Transactions) - -The execution block header's `transactions_root` is updated to match the consensus `ExecutionPayloadHeader`. +#### Consensus `ExecutionPayloadHeader` changes (Transactions) -To compute the new `transactions_root`, the list of individual `SignedTransaction` containers is represented as a SSZ `List`. +The [consensus `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader) is updated for the new `payload.transactions` definition. ```python -block_header.transactions_root == hash_tree_root(List[SignedTransaction, MAX_TRANSACTIONS_PER_PAYLOAD]( - transaction_0, - transaction_1, - transaction_2, - ... -)) +payload_header.transactions_root = hash_tree_root(payload.transactions) ``` -##### Transaction validation +#### Execution block header changes (Transactions) + +The [execution block header's `txs-root`](https://github.com/ethereum/devp2p/blob/bd17dac4228c69b6379644355f373669f74952cd/caps/eth.md#block-encoding-and-validity) is updated to match the consensus `ExecutionPayloadHeader.transactions_root`. + +#### Transaction helpers ```python def get_transaction_sighash(tx: Transaction, hash_version: TxHashVersion) -> bytes: if hash_version == TX_HASH_VERSION_EIP4844: - return keccak([0x05] + hash_tree_root(tx)) + return keccak([0x05] + SSZ.encode(tx)) + assert tx.max_fee_per_data_gas == 0 assert len(tx.blob_versioned_hashes) == 0 if hash_version == TX_HASH_VERSION_EIP1559: @@ -246,12 +230,13 @@ def get_transaction_sighash(tx: Transaction, hash_version: TxHashVersion) -> byt ``` ```python -def get_transaction_id(signed_tx: SignedTransaction) -> bytes: +def encode_signed_transaction(signed_tx: SignedTransaction) -> bytes: hash_version = signed_tx.hash_version if hash_version == TX_HASH_VERSION_EIP4844: - return keccak([0x05] + hash_tree_root(signed_tx)) + return [0x05] + SSZ.encode(signed_tx) + assert tx.max_fee_per_data_gas == 0 assert len(tx.blob_versioned_hashes) == 0 if hash_version == TX_HASH_VERSION_EIP1559: @@ -274,7 +259,7 @@ def get_transaction_id(signed_tx: SignedTransaction) -> bytes: sedes = List([schema for schema, _ in schema]) values = [value for _, value in schema] - return keccak([0x02] + rlp.encode(values, sedes)) + return [0x02] + rlp.encode(values, sedes) assert tx.max_priority_fee_per_gas == tx.max_fee_per_gas @@ -297,7 +282,7 @@ def get_transaction_id(signed_tx: SignedTransaction) -> bytes: sedes = List([schema for schema, _ in schema]) values = [value for _, value in schema] - return keccak([0x01] + rlp.encode(values, sedes)) + return [0x01] + rlp.encode(values, sedes) assert len(tx.access_list) == 0 @@ -316,7 +301,7 @@ def get_transaction_id(signed_tx: SignedTransaction) -> bytes: sedes = List([schema for schema, _ in schema]) values = [value for _, value in schema] - return keccak(rlp.encode(values, sedes)) + return rlp.encode(values, sedes) assert tx.chain_id == 0 @@ -335,50 +320,83 @@ def get_transaction_id(signed_tx: SignedTransaction) -> bytes: sedes = List([schema for schema, _ in schema]) values = [value for _, value in schema] - return keccak(rlp.encode(values, sedes)) + return rlp.encode(values, sedes) assert False ``` ```python -def validate_transaction_payload(payload: TransactionPayload): - if payload.signed_tx.hash_version < TX_HASH_VERSION_EIP4844: - assert len(payload.signed_tx.tx.blob_versioned_hashes) == 0 - - if payload.signed_tx.hash_version < TX_HASH_VERSION_EIP1559: - assert payload.signed_tx.tx.max_priority_fee_per_gas == payload.signed_tx.tx.max_fee_per_gas +def get_transaction_id(signed_tx: SignedTransaction) -> bytes: + return keccak(encode_signed_transaction(signed_tx)) +``` - if payload.signed_tx.hash_version < TX_HASH_VERSION_EIP2930: - assert len(payload.signed_tx.tx.access_list) == 0 +```python +def validate_transaction(signed_tx: SignedTransaction): + hash_version = signed_tx.hash_version - if payload.signed_tx.hash_version < TX_HASH_VERSION_EIP155: - assert payload.signed_tx.tx.chain_id == 0 + if hash_version == TX_HASH_VERSION_EIP4844: + return + assert signed_tx.tx.max_fee_per_data_gas == 0 + assert len(signed_tx.tx.blob_versioned_hashes) == 0 - if len(payload.signed_tx.tx.blob_versioned_hashes) == 0: - assert payload.blob is None - else: - assert payload.blob is not None + if hash_version == TX_HASH_VERSION_EIP1559: + return + assert signed_tx.tx.max_priority_fee_per_gas == signed_tx.tx.max_fee_per_gas - # Based on EIP-4844 logic - versioned_hashes = payload.signed_tx.tx.blob_versioned_hashes - commitments = payload.blob.blob_kzgs - blobs = payload.blob.blobs - # note: assert blobs are not malformatted - assert len(versioned_hashes) == len(commitments) == len(blobs) + if hash_version == TX_HASH_VERSION_EIP2930: + return + assert len(signed_tx.tx.access_list) == 0 - # Verify that commitments match the blobs by checking the KZG proof - assert verify_aggregate_kzg_proof(blobs, commitments, payload.blob.kzg_aggregated_proof) + if hash_version == TX_HASH_VERSION_EIP155: + return + assert signed_tx.tx.chain_id == 0 - # Now that all commitments have been verified, check that versioned_hashes matches the commitments - for versioned_hash, commitment in zip(versioned_hashes, commitments): - assert versioned_hash == kzg_to_versioned_hash(commitment) + if hash_Version == TX_HASH_VERSION_LEGACY: + return + assert False ``` -##### Upgrading transactions to SSZ - ```python -def upgrade_signed_rlp_transaction_to_ssz(rlp_encoded_tx: bytes) -> SignedTransaction: - tx_type = rlp_encoded_tx[0] # EIP-2718 transaction type +def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: + tx_type = encoded_signed_tx[0] # EIP-2718 transaction type + + if tx_type == 0x05: + class BlobTransaction(Container): + chain_id: uint256 + nonce: uint64 + max_priority_fee_per_gas: uint256 + max_fee_per_gas: uint256 + gas: uint64 + to: Union[None, Address] # Address = Bytes20 + value: uint256 + data: ByteList[MAX_CALLDATA_SIZE] + access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE] + max_fee_per_data_gas: uint256 + blob_versioned_hashes: List[VersionedHash, MAX_VERSIONED_HASHES_LIST_SIZE] + + class SignedBlobTransaction(Container): + message: BlobTransaction + signature: ECDSASignature + + pre = SSZ.decode(encoded_signed_tx[1:]) + + return SignedTransaction( + tx=Transaction( + chain_id=pre.message.chain_id, + nonce=premessage.nonce, + max_priority_fee_per_gas=pre.message.max_priority_fee_per_gas, + max_fee_per_gas=premessage.max_fee_per_gas, + gas_limit=pre.message.gas, + to=pre.message.to, + value=pre.message.value, + data=pre.message.data, + access_list=pre.message.access_list, + max_fee_per_data_gas=pre.message.max_fee_per_data_gas, + blob_versioned_hashes=pre.message.blob_versioned_hashes, + ), + signature=pre.signature, + hash_version=TX_HASH_VERSION_EIP4844, + ) if tx_type == 0x02: class SignedEIP1559Transaction(rlp.Serializable): @@ -396,7 +414,7 @@ def upgrade_signed_rlp_transaction_to_ssz(rlp_encoded_tx: bytes) -> SignedTransa ('signature_r', big_endian_int), ('signature_s', big_endian_int), ) - pre = SignedEIP1559Transaction.deserialize(rlp_encoded_tx[1:]) + pre = SignedEIP1559Transaction.deserialize(encoded_signed_tx[1:]) return SignedTransaction( tx=Transaction( @@ -436,7 +454,7 @@ def upgrade_signed_rlp_transaction_to_ssz(rlp_encoded_tx: bytes) -> SignedTransa ('signatureR', big_endian_int), ('signatureS', big_endian_int), ) - pre = SignedEIP2930Transaction.deserialize(rlp_encoded_tx[1:]) + pre = SignedEIP2930Transaction.deserialize(encoded_signed_tx[1:]) return SignedTransaction( tx=Transaction( @@ -462,8 +480,6 @@ def upgrade_signed_rlp_transaction_to_ssz(rlp_encoded_tx: bytes) -> SignedTransa ) if 0xc0 <= tx_type <= 0xfe: - (nonce, gasprice, startgas, to, value, data, chainid, 0, 0) - class SignedLegacyTransaction(rlp.Serializable): fields = ( ('nonce', big_endian_int), @@ -476,7 +492,7 @@ def upgrade_signed_rlp_transaction_to_ssz(rlp_encoded_tx: bytes) -> SignedTransa ('r', big_endian_int), ('s', big_endian_int), ) - pre = SignedLegacyTransaction.deserialize(rlp_encoded_tx) + pre = SignedLegacyTransaction.deserialize(encoded_signed_tx) if pre.v not in (27, 28): return SignedTransaction( @@ -521,6 +537,12 @@ def upgrade_signed_rlp_transaction_to_ssz(rlp_encoded_tx: bytes) -> SignedTransa ### Receipts +Each receipt is linked to the transaction at the same sequential index within a block. The underlying [EIP-2718](./eip-2718.md) transaction type can be inferred from the corresponding transaction. + +#### Consensus `ExecutionPayload` changes (Receipts) + +A new `Receipt` SSZ container is introduced to represent receipts. + | Name | SSZ equivalent | | - | - | | `Topic` | `Bytes32` | @@ -534,7 +556,7 @@ def upgrade_signed_rlp_transaction_to_ssz(rlp_encoded_tx: bytes) -> SignedTransa | `MAX_RECEIPTS_PER_PAYLOAD` | `uint64(2**20)` (= 1,048,576) | Maximum amount of receipts allowed in each block | ```python -class Log(Container): +class ReceiptLog(Container): address: Address topics: List[Topic, MAX_TOPICS_PER_LOG] data: ByteVector[MAX_LOG_DATA_SIZE] @@ -543,13 +565,22 @@ class Receipt(Container): status: uint256 # EIP-658 cumulative_transaction_gas_used: uint64 logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] - logs: List[Log, MAX_LOGS_PER_RECEIPT] + logs: List[ReceiptLog, MAX_LOGS_PER_RECEIPT] ``` -The `blocks.receipts_root` is updated to a SSZ root. To compute the new `receipts_root`, the list of individual `Receipt` containers is represented as a SSZ `List`. +The [consensus `ExecutionPayload`'s `receipts_root`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) now refers to an SSZ [`Root`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types) instead of an MPT [`Hash32`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types). ```python -block.receipts_root == hash_tree_root(List[Receipt, MAX_RECEIPTS_PER_PAYLOAD]( +class ExecutionPayload(Container): + ... + receipts_root: Root + ... +``` + +To compute the new `receipts_root`, the list of individual `Receipt` containers is represented as a SSZ `List`. + +```python +payload.receipts_root = hash_tree_root(List[Receipt, MAX_TRANSACTIONS_PER_PAYLOAD]( receipt_0, receipt_1, receipt_2, @@ -557,36 +588,93 @@ block.receipts_root == hash_tree_root(List[Receipt, MAX_RECEIPTS_PER_PAYLOAD]( )) ``` -#### Consensus `ExecutionPayload` changes (Receipts) +#### Consensus `ExecutionPayloadHeader` changes (Receipts) -The [consensus `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) and [consensus `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader) are updated to take account the new consensus `receipts_root` type. +The [consensus `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader) is updated for the new `payload.receipts_root` definition. ```python -class ExecutionPayload(Container): - ... - receipts_root: Root # Formerly, `Bytes32` - ... - - class ExecutionPayloadHeader(Container): ... - receipts_root: Root # Formerly, `Bytes32` + receipts_root: Root ... ``` -#### Execution block header changes (Receipts) +```python +payload_header.receipts_root = payload.receipts_root +``` + +#### Execution block header changes (Transactions) + +The [execution block header's `receipts-root`](https://github.com/ethereum/devp2p/blob/bd17dac4228c69b6379644355f373669f74952cd/caps/eth.md#block-encoding-and-validity) is updated to match the consensus `ExecutionPayloadHeader.receipts_root`. + +#### Receipt helpers -The execution block header's `receipts_root` is updated to match the consensus `ExecutionPayloadHeader`. +```python +def encode_receipt(receipt: Receipt, hash_version: TxHashVersion) -> bytes: + schema = ( + (big_endian_int, receipt.status), + (big_endian_int, receipt.cumulative_transaction_gas_used), + (Binary[256, 256], receipt.logs_bloom), + (List([Binary[20, 20], List([Binary[32, 32]]), Binary[0, uint64(2**24)]]), [ + (log.address, log.topics, log.data) for log in receipt.logs + ]), + ) + sedes = List([schema for schema, _ in schema]) + values = [value for _, value in schema] + encoded = rlp.encode(values, sedes) + + if hash_version == TX_HASH_VERSION_EIP4844: + return [0x05] + encoded + if hash_version == TX_HASH_VERSION_EIP1559: + return [0x02] + encoded + if hash_version == TX_HASH_VERSION_EIP2930: + return [0x01] + encoded + if hash_version == TX_HASH_VERSION_EIP155: + return encoded + if hash_Version == TX_HASH_VERSION_LEGACY: + return encoded + assert False +``` ```python -block_header.receipts_root = block.receipts_root +def decode_receipt(encoded_receipt: bytes) -> Receipt: + tx_type = encoded_receipt[0] + + class RLPReceipt(rlp.Serializable): + fields = ( + ('status', big_endian_int), + ('cumulative_transaction_gas_used', big_endian_int), + ('logs_bloom', Binary[256, 256]), + ('logs', List([Binary[20, 20], List([Binary[32, 32]]), Binary[0, uint64(2**24)]])), + ) + if tx_type == 0x05: + pre = RLPReceipt.deserialize(encoded_receipt[1:]) + elif tx_type == 0x02: + pre = RLPReceipt.deserialize(encoded_receipt[1:]) + elif tx_type == 0x01: + pre = RLPReceipt.deserialize(encoded_receipt[1:]) + elif 0xc0 <= tx_type <= 0xfe: + pre = RLPReceipt.deserialize(encoded_receipt) + else: + assert False + + return Receipt( + status=pre.status, + cumulative_transaction_gas_used=pre.cumulative_transaction_gas_used, + logs_bloom=pre.logs_bloom, + logs=[ReceiptLog( + address=log[0], + topics=log[1], + data=log[2], + ) for log in pre.logs], + ) ``` ### Withdrawals -#### Execution block changes (Withdrawals) +#### Execution block header changes (Withdrawals) -The individual [`Withdrawal` objects](./eip-4895.md) within the execution block's `withdrawals` list are now represented as SSZ `Container`s, matching the definition of the [consensus-specs `Withdrawal`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/capella/beacon-chain.md#withdrawal). +The existing consensus [`Withdrawal`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/capella/beacon-chain.md#withdrawal) SSZ container is used to represent withdrawals. ```python class Withdrawal(Container): @@ -596,22 +684,7 @@ class Withdrawal(Container): amount: Gwei ``` -The `blocks.withdrawals` list remains RLP encoded: - -```python -block.withdrawals = RLP([ - SSZ.encode(withdrawal_0), - SSZ.encode(withdrawal_1), - SSZ.encode(withdrawal_2), - ... -]) -``` - -#### Execution block header changes (Withdrawals) - -The execution block header's `withdrawals_root` is updated to the `withdrawals_root` definition of the [consensus-specs `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader). - -To compute the new `withdrawals_root`, the list of individual `Withdrawal` containers is represented as a SSZ `List`. +The execution block header's `withdrawals-root` is updated to match the consensus [`ExecutionPayloadHeader.withdrawals_root`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader). | Name | Value | Description | | - | - | - | @@ -626,11 +699,39 @@ block_header.withdrawals_root == hash_tree_root(List[Withdrawal, MAX_WITHDRAWALS )) ``` -### Deposits +#### Withdrawal helpers -Same as withdrawals, but ideally we have SSZ by the time deposits arrive. +```python +def encode_withdrawal(withdrawal: Withdrawal) -> bytes: + schema = ( + (big_endian_int, withdrawal.index), + (big_endian_int, withdrawal.validator_index), + (Binary[20, 20], withdrawal.address), + (big_endian_int, withdrawal.amount), + ) + sedes = List([schema for schema, _ in schema]) + values = [value for _, value in schema] + return rlp.encode(values, sedes) +``` + +```python +def decode_withdrawal(encoded_withdrawal: bytes) -> Withdrawal: + class RLPWithdrawal(rlp.Serializable): + fields = ( + ('index', bid_endian_int), + ('validator_index', big_endian_int), + ('address', Binary[20, 20]), + ('amount', big_endian_int), + ) + pre = RLPWithdrawal.deserialize(encoded_withdrawal) -Also, if more MPT are introduced by other EIP, same story. + return Withdrawal( + index=pre.index, + validator_index=pre.validator_index, + address=pre.address, + amount=pre.amount, + ) +``` ## Rationale @@ -665,7 +766,6 @@ Applications that rely on the replaced MPT root commitments in the block header ### SSZ tests (Transactions) ```python -# Without EIP-4844 blobs SSZ.encode(TransactionPayload( signed_tx=SignedTransaction( tx=Transaction( @@ -698,10 +798,6 @@ SSZ.encode(TransactionPayload( hash_version=TxHashVersion(0x04), ), )) == fromhex( - # offset(payload.signed_tx) - "08000000" + - # offset(payload.blob) (unused) - "63010000" + # offset(signed_tx.tx) "46000000" + # signed_tx.signature @@ -721,114 +817,17 @@ SSZ.encode(TransactionPayload( # tx.gas_limit "0500000000000000" + # offset(tx.to) - "88000000" + + "a8000000" + # tx.value "0600000000000000000000000000000000000000000000000000000000000000" + # offset(tx.data) - "9c000000" + + "bc000000" + # offset(tx.access_list) - "9d000000" + - # offset(tx.blob_versioned_hashes) (unused) - "15010000" + - # tx.to - "0000000000000000000000000000000000000008" + - # tx.data - "09" + - # offset(tx.access_tuple[0]) - "08000000" + - # offset(tx.access_tuple[1]) - "60000000" + - # access_tuple[0].address - "000000000000000000000000000000000000000b" + - # offset(access_tuple[0].storage_keys) - "18000000" + - # access_tuple[0].storage_keys[0] - "000000000000000000000000000000000000000000000000000000000000000c" + - # access_tuple[0].storage_keys[1] - "000000000000000000000000000000000000000000000000000000000000000d" + - # access_tuple[1].address - "000000000000000000000000000000000000000e" + - # offset(access_tuple[1].storage_keys) - "18000000" -) - -# With EIP-4844 blobs -SSZ.encode(TransactionPayload( - signed_tx=SignedTransaction( - tx=Transaction( - chain_id=uint64(0x07), - nonce=uint64(0x0a), - max_priority_fee_per_gas=uint256(0x0f), - max_fee_per_gas=uint256(0x04), - gas_limit=uint64(0x05), - to=fromhex("0000000000000000000000000000000000000008"), - value=uint256(0x06), - data=[0x09], - access_list=[ - AccessTuple( - address=fromhex("000000000000000000000000000000000000000b"), - storage_keys=[ - fromhex("000000000000000000000000000000000000000000000000000000000000000c"), - fromhex("000000000000000000000000000000000000000000000000000000000000000d"), - ], - ), - AccessTuple( - address=fromhex("000000000000000000000000000000000000000e"), - ), - ], - blob_versioned_hashes=[ - fromhex("0000000000000000000000000000000000000000000000000000000000000010"), - ], - ), - signature=ECDSASignature( - y_parity=true, - r=uint256(0x02), - s=uint256(0x03), - ), - hash_version=TxHashVersion(0x04), - ), - blob=BlobTransactionExtension( - blob_kzgs=[ - [0x11] + [0x00] * 47, - ], - blobs=[ - [[uint256(0x12)] + [uint256(0x00)] * 4_095], - ], - kzg_aggregated_proof=[0x13] + [0x00] * 47, - ) -)) == fromhex( - # offset(payload.signed_tx) - "08000000" + - # offset(payload.blob) - "83010000" + - # offset(signed_tx.tx) - "46000000" + - # signed_tx.signature - "01" + - "0200000000000000000000000000000000000000000000000000000000000000" + - "0300000000000000000000000000000000000000000000000000000000000000" + - # signed_tx.hash_version - "04" + - # tx.chain_id - "0700000000000000" + - # tx.nonce - "0a00000000000000" + - # tx.max_priority_fee_per_gas - "0f00000000000000000000000000000000000000000000000000000000000000" + - # tx.max_fee_per_gas - "0400000000000000000000000000000000000000000000000000000000000000" + - # tx.gas_limit - "0500000000000000" + - # offset(tx.to) - "88000000" + - # tx.value - "0600000000000000000000000000000000000000000000000000000000000000" + - # offset(tx.data) - "9c000000" + - # offset(tx.access_list) - "9d000000" + + "bd000000" + + # tx.max_fee_per_data_gas + "0000000000000000000000000000000000000000000000000000000000000000" + # offset(tx.blob_versioned_hashes) - "15010000" + + "35010000" + # tx.to "0000000000000000000000000000000000000008" + # tx.data @@ -848,23 +847,7 @@ SSZ.encode(TransactionPayload( # access_tuple[1].address "000000000000000000000000000000000000000e" + # offset(access_tuple[1].storage_keys) - "18000000" + - # blob_versioned_hashes[0] - "0000000000000000000000000000000000000000000000000000000000000010" + - # offset(blob.blob_kzgs) - "38000000" + - # offset(blob.blobs) - "68000000" + - # blob.kzg_aggregated_proof - "130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + - # blob.blob_kzgs[0] - "110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + - # blob.blobs[0] - "1200000000000000000000000000000000000000000000000000000000000000" + - "0000000000000000000000000000000000000000000000000000000000000000" + - "0000000000000000000000000000000000000000000000000000000000000000" + - ... - "0000000000000000000000000000000000000000000000000000000000000000" + "18000000" ) ``` From 3f999e29c0663192ed74a57f9eb1ee90dd7e3fdf Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Thu, 2 Feb 2023 22:03:33 +0100 Subject: [PATCH 10/25] Fix section header --- EIPS/eip-6404.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 47e6959ec80584..9600d9f61ac83f 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -603,7 +603,7 @@ class ExecutionPayloadHeader(Container): payload_header.receipts_root = payload.receipts_root ``` -#### Execution block header changes (Transactions) +#### Execution block header changes (Receipts) The [execution block header's `receipts-root`](https://github.com/ethereum/devp2p/blob/bd17dac4228c69b6379644355f373669f74952cd/caps/eth.md#block-encoding-and-validity) is updated to match the consensus `ExecutionPayloadHeader.receipts_root`. From 2c2943405f7e930b1a6be2e37fb33953eea1905d Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Thu, 2 Feb 2023 22:42:36 +0100 Subject: [PATCH 11/25] Add `transaction_hashes_root`. --- EIPS/eip-6404.md | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 9600d9f61ac83f..a646cfc0b68aaa 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -51,7 +51,7 @@ TBD ### Transactions -This design proposes a single normalized `Transaction` type. To include a transaction from an earlier type into a block, it needs to be migrated using `upgrade_signed_rlp_transaction_to_ssz`. For the purpose of signature validation, the originally signed transaction hash can be recomputed using `get_transaction_sighash`. For applications that need a perpetual identifier, `get_transaction_id` can be used. +This design proposes a single normalized `Transaction` type. To include a transaction from an earlier type into a block, it needs to be migrated using `upgrade_signed_rlp_transaction_to_ssz`. For the purpose of signature validation, the originally signed transaction hash can be recomputed using `get_transaction_sighash`. For applications that need a perpetual identifier, `get_transaction_hash` can be used. #### Hash version @@ -119,6 +119,8 @@ class SignedTransaction(Container): The [consensus `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) is updated to use the new `SignedTransaction` SSZ container. +Furthermore, it is extended with a new `transaction_hashes_root` field to commit to the perpetual transaction hashes within the block. + | Name | Value | Description | | - | - | - | | [`MAX_TRANSACTIONS_PER_PAYLOAD`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#execution) | `uint64(2**20)` (= 1,048,576) | Maximum amount of transactions allowed in each block | @@ -128,20 +130,37 @@ class ExecutionPayload(Container): ... transactions: List[SignedTransaction, MAX_TRANSACTIONS_PER_PAYLOAD] ... + transaction_hashes_root: Root +``` + +To compute the new `transaction_hashes_root`, the list of the transaction hashes of individual `SignedTransaction` containers is represented as a SSZ `List`. + +```python +payload.transaction_hashes_root = hash_tree_root(List[Hash32, MAX_TRANSACTIONS_PER_PAYLOAD]( + get_transaction_hash(transaction_0), + get_transaction_hash(transaction_1), + get_transaction_hash(transaction_2), + ... +)) ``` #### Consensus `ExecutionPayloadHeader` changes (Transactions) -The [consensus `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader) is updated for the new `payload.transactions` definition. +The [consensus `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader) is updated for the new `ExecutionPayload.transactions` definition. + +Likewise, it is extended with a new `transaction_hashes_root` field matching the new `ExecutionPayload.transaction_hashes_root` definition. ```python payload_header.transactions_root = hash_tree_root(payload.transactions) +payload_header.transaction_hashes_root = payload.transaction_hashes_root ``` #### Execution block header changes (Transactions) The [execution block header's `txs-root`](https://github.com/ethereum/devp2p/blob/bd17dac4228c69b6379644355f373669f74952cd/caps/eth.md#block-encoding-and-validity) is updated to match the consensus `ExecutionPayloadHeader.transactions_root`. +Furthermore, the execution block header is extended with a new `txs-hashes-root` field matching the `ExecutionPayloadHeader.transaction_hashes_root` definition. + #### Transaction helpers ```python @@ -326,7 +345,7 @@ def encode_signed_transaction(signed_tx: SignedTransaction) -> bytes: ``` ```python -def get_transaction_id(signed_tx: SignedTransaction) -> bytes: +def get_transaction_hash(signed_tx: SignedTransaction) -> bytes: return keccak(encode_signed_transaction(signed_tx)) ``` @@ -590,7 +609,7 @@ payload.receipts_root = hash_tree_root(List[Receipt, MAX_TRANSACTIONS_PER_PAYLOA #### Consensus `ExecutionPayloadHeader` changes (Receipts) -The [consensus `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader) is updated for the new `payload.receipts_root` definition. +The [consensus `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader) is updated for the new `ExecutionPayload.receipts_root` definition. ```python class ExecutionPayloadHeader(Container): @@ -743,19 +762,25 @@ def decode_withdrawal(encoded_withdrawal: bytes) -> Withdrawal: - **Shorter merkle proofs:** Compared to approaches based on SSZ `Onion` (merkleized as \[o]bject, serialized as u\[nion]), there is less nesting in the merkle tree. This makes proofs shorter (32 bytes per nesting level) -- **Easy to deprecate legacy transactions:** If a transaction type is no longer allowed, the tree shape remains static. In non-archive settings, the `upgrade_signed_rlp_transaction_to_ssz`, `get_transaction_id` and `get_transaction_sighash` functions can be adjusted to drop support for legacy transaction types. +- **Easy to deprecate legacy transactions:** If a transaction type is no longer allowed, the tree shape remains static. In non-archive settings, the `upgrade_signed_rlp_transaction_to_ssz`, `get_transaction_hash` and `get_transaction_sighash` functions can be adjusted to drop support for legacy transaction types. - **Discourage legacy transaction types:** There are no block size benefits to use a legacy transaction type anymore. A penalty fee could be charged to new legacy transactions to promote timely upgrade to latest `Transaction` format. - **Prior art:** Multiple modules of Ethereum already process common fields in a unified way. The consensus pytests use `is_post_fork` to conditionally enable logic. The execution JSON-RPC reports transaction fields under the same name regardless of type. The consensus light client protocol incorporates a very similar mechanism for upgrading consensus `ExecutionPayloadHeader` to later formats: [`get_transaction_sighash` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/sync-protocol.md#modified-get_lc_execution_root) / [`upgrade_to_latest` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/full-node.md#modified-block_to_light_client_header). +### Why the need for `transaction_hashes_root`? + +The perpetual transaction hash is used by many applications to uniquely identify a transaction. The `transaction_hashes_root` allows smart contracts to verify proofs about objects that are linked to the perpetual transaction hash, without having to re-hash the entire transaction according to the original `TxHashVersion`. + +For example, when requesting the `Receipt` for a given transaction hash, the response could include (1) the `Receipt` itself, (2) the index of the `Receipt` within `payload.receipts`, (3) the SSZ merkle proof that the `Receipt` is indeed located at that index, (4) the SSZ merkle proof that the transaction with the same index actually has the requested perpetual transaction hash. + ## Backwards Compatibility Applications that solely rely on the legacy RLP encoding but do not rely on the MPT commitment in the block header can still be used through a re-encoding proxy. Applications that rely on the replaced MPT root commitments in the block header can no longer find that information. Analysis is required whether affected applications have a migration path available. If major applications can't migrate, can keep the legacy MPT commitment in the header for now, and introduce the new SSZ trees next to them. -- **Transactions:** The txid is commonly used by block explorers. A helper function `get_transaction_id` is specified to replicate historic transaction IDs. +- **Transactions:** The transaction hash is commonly used by block explorers. A helper function `get_transaction_hash` is specified to replicate historic transaction IDs. - **Receipts:** The concept of a "receipt ID" is not commonly used. There is also no JSON-RPC endpoint to obtain a receipts proof. It is not expected that major applications rely on the legacy MPT for receipts. From f7d3a2370fb7aeb7fee0ef8b1f08022322d3f5c7 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Fri, 3 Feb 2023 01:55:29 +0100 Subject: [PATCH 12/25] rm fixed container capacity discussion from this EIP --- EIPS/eip-6404.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index a646cfc0b68aaa..9b0416c7d5e122 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -41,14 +41,6 @@ A new [SSZ type](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562 - If value is not `None`, serialize as `T`, and merkleize as `List[T, 1]` with length `1`. - Serialize `Optional[T]` as variable-size object (create offset-table entry in enclosing containers). -#### Fixed capacity `Container` (TBD) - -The merkleization for SSZ `Container` structures generates a tree of minimum depth to contain all leaves. This is problematic whenever the number of leaves changes to require a different power of 2, as all `GeneralizedIndex` values change. - -SSZ `Container` structures should be annotated with a maximum capacity, similar to SSZ `List`. Unused space would be zero-extended accordingly. - -TBD - ### Transactions This design proposes a single normalized `Transaction` type. To include a transaction from an earlier type into a block, it needs to be migrated using `upgrade_signed_rlp_transaction_to_ssz`. For the purpose of signature validation, the originally signed transaction hash can be recomputed using `get_transaction_sighash`. For applications that need a perpetual identifier, `get_transaction_hash` can be used. From b2fe0eb56ded26f7074f317d45d96dbd543a09d6 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Fri, 3 Feb 2023 01:55:43 +0100 Subject: [PATCH 13/25] Convert to `BlobTransaction` for sighash/hash computation --- EIPS/eip-6404.md | 53 ++++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 9b0416c7d5e122..70a95d8c41a28f 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -155,10 +155,24 @@ Furthermore, the execution block header is extended with a new `txs-hashes-root` #### Transaction helpers +These helpers use `BlobTransaction` and `SignedBlobTransaction` as defined in [EIP-4844](./eip-4844.md). + ```python def get_transaction_sighash(tx: Transaction, hash_version: TxHashVersion) -> bytes: if hash_version == TX_HASH_VERSION_EIP4844: - return keccak([0x05] + SSZ.encode(tx)) + return keccak([0x05] + SSZ.encode(BlobTransaction( + chain_id=tx.chain_id, + nonce=tx.nonce, + max_priority_fee_per_gas=tx.max_priority_fee_per_gas, + max_fee_per_gas=tx.max_fee_per_gas, + gas=tx.gas_limit, + to=tx.to, + value=tx.value, + data=tx.data, + access_list=tx.access_list, + max_fee_per_data_gas=tx.max_fee_per_data_gas, + blob_versioned_hashes=tx.blob_versioned_hashes, + ))) assert tx.max_fee_per_data_gas == 0 assert len(tx.blob_versioned_hashes) == 0 @@ -245,7 +259,21 @@ def encode_signed_transaction(signed_tx: SignedTransaction) -> bytes: hash_version = signed_tx.hash_version if hash_version == TX_HASH_VERSION_EIP4844: - return [0x05] + SSZ.encode(signed_tx) + return [0x05] + SSZ.encode(SignedBlobTransaction( + message=BlobTransaction( + chain_id=signed_tx.tx.chain_id, + nonce=signed_tx.tx.nonce, + max_priority_fee_per_gas=signed_tx.tx.max_priority_fee_per_gas, + max_fee_per_gas=signed_tx.tx.max_fee_per_gas, + gas=signed_tx.tx.gas_limit, + to=signed_tx.tx.to, + value=signed_tx.tx.value, + data=signed_tx.tx.data, + access_list=signed_tx.tx.access_list, + max_fee_per_data_gas=signed_tx.tx.max_fee_per_data_gas, + blob_versioned_hashes=signed_tx.tx.blob_versioned_hashes, + signature=signed_tx.signature, + ))) assert tx.max_fee_per_data_gas == 0 assert len(tx.blob_versioned_hashes) == 0 @@ -372,24 +400,7 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: tx_type = encoded_signed_tx[0] # EIP-2718 transaction type if tx_type == 0x05: - class BlobTransaction(Container): - chain_id: uint256 - nonce: uint64 - max_priority_fee_per_gas: uint256 - max_fee_per_gas: uint256 - gas: uint64 - to: Union[None, Address] # Address = Bytes20 - value: uint256 - data: ByteList[MAX_CALLDATA_SIZE] - access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE] - max_fee_per_data_gas: uint256 - blob_versioned_hashes: List[VersionedHash, MAX_VERSIONED_HASHES_LIST_SIZE] - - class SignedBlobTransaction(Container): - message: BlobTransaction - signature: ECDSASignature - - pre = SSZ.decode(encoded_signed_tx[1:]) + pre = SSZ.decode_ssz(SignedBlobTransaction, encoded_signed_tx[1:]) return SignedTransaction( tx=Transaction( @@ -780,6 +791,8 @@ Applications that rely on the replaced MPT root commitments in the block header ## Test Cases +TBD + ### SSZ tests (Transactions) ```python From 09ed60584623208203e3e04469a76d9e3fc4b6ce Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Fri, 3 Feb 2023 02:37:30 +0100 Subject: [PATCH 14/25] Further cleanup --- EIPS/eip-6404.md | 156 ++--------------------------------------------- 1 file changed, 6 insertions(+), 150 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 70a95d8c41a28f..56b6b195cfa58f 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -43,7 +43,7 @@ A new [SSZ type](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562 ### Transactions -This design proposes a single normalized `Transaction` type. To include a transaction from an earlier type into a block, it needs to be migrated using `upgrade_signed_rlp_transaction_to_ssz`. For the purpose of signature validation, the originally signed transaction hash can be recomputed using `get_transaction_sighash`. For applications that need a perpetual identifier, `get_transaction_hash` can be used. +This design proposes a single normalized `Transaction` type. To include a transaction from an earlier type into a block, it needs to be migrated using `decode_signed_transaction`. For the purpose of signature validation, the originally signed transaction hash can be recomputed using `get_transaction_sighash`. For applications that need a perpetual identifier, `get_transaction_hash` can be used. #### Hash version @@ -765,9 +765,7 @@ def decode_withdrawal(encoded_withdrawal: bytes) -> Withdrawal: - **Shorter merkle proofs:** Compared to approaches based on SSZ `Onion` (merkleized as \[o]bject, serialized as u\[nion]), there is less nesting in the merkle tree. This makes proofs shorter (32 bytes per nesting level) -- **Easy to deprecate legacy transactions:** If a transaction type is no longer allowed, the tree shape remains static. In non-archive settings, the `upgrade_signed_rlp_transaction_to_ssz`, `get_transaction_hash` and `get_transaction_sighash` functions can be adjusted to drop support for legacy transaction types. - -- **Discourage legacy transaction types:** There are no block size benefits to use a legacy transaction type anymore. A penalty fee could be charged to new legacy transactions to promote timely upgrade to latest `Transaction` format. +- **Easy to deprecate legacy transactions:** If a transaction type is no longer allowed, the tree shape remains static. In non-archive settings, the `decode_signed_transaction`, `get_transaction_hash` and `get_transaction_sighash` functions can be adjusted to drop support for legacy transaction types. - **Prior art:** Multiple modules of Ethereum already process common fields in a unified way. The consensus pytests use `is_post_fork` to conditionally enable logic. The execution JSON-RPC reports transaction fields under the same name regardless of type. The consensus light client protocol incorporates a very similar mechanism for upgrading consensus `ExecutionPayloadHeader` to later formats: [`get_transaction_sighash` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/sync-protocol.md#modified-get_lc_execution_root) / [`upgrade_to_latest` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/full-node.md#modified-block_to_light_client_header). @@ -781,160 +779,18 @@ For example, when requesting the `Receipt` for a given transaction hash, the res Applications that solely rely on the legacy RLP encoding but do not rely on the MPT commitment in the block header can still be used through a re-encoding proxy. -Applications that rely on the replaced MPT root commitments in the block header can no longer find that information. Analysis is required whether affected applications have a migration path available. If major applications can't migrate, can keep the legacy MPT commitment in the header for now, and introduce the new SSZ trees next to them. +Applications that rely on the replaced MPT root commitments in the block header can no longer find that information. Analysis is required whether affected applications have a migration path available to use the SSZ root commitments instead. -- **Transactions:** The transaction hash is commonly used by block explorers. A helper function `get_transaction_hash` is specified to replicate historic transaction IDs. +- **Transactions:** The perpetual transaction hash is commonly used by block explorers. A helper function `get_transaction_hash` is specified to replicate historic transaction hashes. -- **Receipts:** The concept of a "receipt ID" is not commonly used. There is also no JSON-RPC endpoint to obtain a receipts proof. It is not expected that major applications rely on the legacy MPT for receipts. +- **Receipts:** Receipts are tied to transactions, so are typically tied to either the transaction hash, or to a block hash + sequential transaction index. There is no JSON-RPC endpoint to obtain a receipts proof. It is not expected that major applications rely on the legacy MPT for receipts. -- **Withdrawals:** Withdrawals were introduced in Capella/Shanghai. It is not expected that major applications rely on the legacy MPT for withdrawals. +- **Withdrawals:** Withdrawals were introduced in Capella/Shanghai just recently. It is not expected that major applications already rely on the legacy MPT for withdrawals. ## Test Cases TBD -### SSZ tests (Transactions) - -```python -SSZ.encode(TransactionPayload( - signed_tx=SignedTransaction( - tx=Transaction( - chain_id=uint64(0x07), - nonce=uint64(0x0a), - max_priority_fee_per_gas=uint256(0x0f), - max_fee_per_gas=uint256(0x04), - gas_limit=uint64(0x05), - to=fromhex("0000000000000000000000000000000000000008"), - value=uint256(0x06), - data=[0x09], - access_list=[ - AccessTuple( - address=fromhex("000000000000000000000000000000000000000b"), - storage_keys=[ - fromhex("000000000000000000000000000000000000000000000000000000000000000c"), - fromhex("000000000000000000000000000000000000000000000000000000000000000d"), - ], - ), - AccessTuple( - address=fromhex("000000000000000000000000000000000000000e"), - ), - ], - ), - signature=ECDSASignature( - y_parity=true, - r=uint256(0x02), - s=uint256(0x03), - ), - hash_version=TxHashVersion(0x04), - ), -)) == fromhex( - # offset(signed_tx.tx) - "46000000" + - # signed_tx.signature - "01" + - "0200000000000000000000000000000000000000000000000000000000000000" + - "0300000000000000000000000000000000000000000000000000000000000000" + - # signed_tx.hash_version - "04" + - # tx.chain_id - "0700000000000000" + - # tx.nonce - "0a00000000000000" + - # tx.max_priority_fee_per_gas - "0f00000000000000000000000000000000000000000000000000000000000000" + - # tx.max_fee_per_gas - "0400000000000000000000000000000000000000000000000000000000000000" + - # tx.gas_limit - "0500000000000000" + - # offset(tx.to) - "a8000000" + - # tx.value - "0600000000000000000000000000000000000000000000000000000000000000" + - # offset(tx.data) - "bc000000" + - # offset(tx.access_list) - "bd000000" + - # tx.max_fee_per_data_gas - "0000000000000000000000000000000000000000000000000000000000000000" + - # offset(tx.blob_versioned_hashes) - "35010000" + - # tx.to - "0000000000000000000000000000000000000008" + - # tx.data - "09" + - # offset(tx.access_tuple[0]) - "08000000" + - # offset(tx.access_tuple[1]) - "60000000" + - # access_tuple[0].address - "000000000000000000000000000000000000000b" + - # offset(access_tuple[0].storage_keys) - "18000000" + - # access_tuple[0].storage_keys[0] - "000000000000000000000000000000000000000000000000000000000000000c" + - # access_tuple[0].storage_keys[1] - "000000000000000000000000000000000000000000000000000000000000000d" + - # access_tuple[1].address - "000000000000000000000000000000000000000e" + - # offset(access_tuple[1].storage_keys) - "18000000" -) -``` - -### SSZ tests (Receipts) - -TBD - -### SSZ tests (Withdrawals) - -```python -withdrawal = Withdrawal( - index=WithdrawalIndex(0x0123), - validator_index=ValidatorIndex(0x4567), - address=fromhex("0001020304050607080910111213141516171819"), - amount=Gwei(0x89ab), -) - -SSZ.encode(withdrawal) == fromhex( - "2301000000000000" + - "6745000000000000" + - "0001020304050607080910111213141516171819" + - "ab89000000000000" -) - -hash_tree_root(withdrawal) == sha256( - sha256(fromhex( - "2301000000000000000000000000000000000000000000000000000000000000" + - "6745000000000000000000000000000000000000000000000000000000000000")) + - sha256(fromhex( - "0001020304050607080910111213141516171819000000000000000000000000" + - "ab89000000000000000000000000000000000000000000000000000000000000")) -) == fromhex("af8247942122ba68d69d2784dfd9994fa16681ff6feb2680d7922c61a899a20d") -``` - -```python -withdrawals = List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD](withdrawal_0, withdrawal_1, withdrawal_2) - -block_header.withdrawals_root == hash_tree_root(withdrawals) == sha256( - sha256( - sha256( - sha256( - sha256(hash_tree_root(withdrawal_0) + hash_tree_root(withdrawal_1)) + - sha256(hash_tree_root(withdrawal_2) + hash_tree_root(Withdrawal()))) + - sha256( - sha256(hash_tree_root(Withdrawal()) + hash_tree_root(Withdrawal())) + - sha256(hash_tree_root(Withdrawal()) + hash_tree_root(Withdrawal())))) + - sha256( - sha256( - sha256(hash_tree_root(Withdrawal()) + hash_tree_root(Withdrawal())) + - sha256(hash_tree_root(Withdrawal()) + hash_tree_root(Withdrawal()))) + - sha256( - sha256(hash_tree_root(Withdrawal()) + hash_tree_root(Withdrawal())) + - sha256(hash_tree_root(Withdrawal()) + hash_tree_root(Withdrawal()))))) + - fromhex("0300000000000000000000000000000000000000000000000000000000000000") # len -) -``` - ## Reference Implementation TBD From ffb67aa75afbd316173ca9249bc2e205efac59f4 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Fri, 3 Feb 2023 09:27:57 +0100 Subject: [PATCH 15/25] Align `SignedTransaction` field order with serialization --- EIPS/eip-6404.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 56b6b195cfa58f..ceb1fac79fef38 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -104,9 +104,9 @@ class ECDSASignature(Container): s: uint256 class SignedTransaction(Container): + hash_version: TxHashVersion tx: Transaction signature: ECDSASignature - hash_version: TxHashVersion ``` The [consensus `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) is updated to use the new `SignedTransaction` SSZ container. @@ -403,6 +403,7 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: pre = SSZ.decode_ssz(SignedBlobTransaction, encoded_signed_tx[1:]) return SignedTransaction( + hash_version=TX_HASH_VERSION_EIP4844, tx=Transaction( chain_id=pre.message.chain_id, nonce=premessage.nonce, @@ -417,7 +418,6 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: blob_versioned_hashes=pre.message.blob_versioned_hashes, ), signature=pre.signature, - hash_version=TX_HASH_VERSION_EIP4844, ) if tx_type == 0x02: @@ -439,6 +439,7 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: pre = SignedEIP1559Transaction.deserialize(encoded_signed_tx[1:]) return SignedTransaction( + hash_version=TX_HASH_VERSION_EIP1559, tx=Transaction( chain_id=pre.chain_id, nonce=pre.nonce, @@ -458,7 +459,6 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: r=pre.signature_r, s=pre.signature_s, ), - hash_version=TX_HASH_VERSION_EIP1559, ) if tx_type == 0x01: @@ -479,6 +479,7 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: pre = SignedEIP2930Transaction.deserialize(encoded_signed_tx[1:]) return SignedTransaction( + hash_version=TX_HASH_VERSION_EIP2930, tx=Transaction( chain_id=pre.chainId, nonce=pre.nonce, @@ -498,7 +499,6 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: r=pre.signatureR, s=pre.signatureS, ), - hash_version=TX_HASH_VERSION_EIP2930, ) if 0xc0 <= tx_type <= 0xfe: @@ -518,6 +518,7 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: if pre.v not in (27, 28): return SignedTransaction( + hash_version=TX_HASH_VERSION_EIP155, tx=Transaction( chain_id=(pre.v - 35) >> 1, nonce=pre.nonce, @@ -533,10 +534,10 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: r=pre.r, s=pre.s, ), - hash_version=TX_HASH_VERSION_EIP155, ) return SignedTransaction( + hash_version=TX_HASH_VERSION_LEGACY, tx=Transaction( nonce=pre.nonce, max_priority_fee_per_gas=pre.gasprice, @@ -551,7 +552,6 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: r=pre.r, s=pre.s, ), - hash_version=TX_HASH_VERSION_LEGACY, ) assert False From 6063d502f314d98943cb05f4b619a7df55d5a618 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Fri, 3 Feb 2023 10:37:44 +0100 Subject: [PATCH 16/25] Write permissions for Vitalik --- EIPS/eip-6404.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index ceb1fac79fef38..9fc851e4067419 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -2,7 +2,7 @@ eip: 6404 title: SSZ transactions, receipts, and withdrawals description: Migration of MPT commitments for transactions, receipts, and withdrawals to SSZ -author: Etan Kissling (@etan-status) +author: Etan Kissling (@etan-status), Vitalik Buterin (@vbuterin) discussions-to: https://ethereum-magicians.org/t/eip-6404-ssz-transactions-receipts-and-withdrawals/12783 status: Draft type: Standards Track From e6d1f86a37e6e7d99f00d521a69e40409e25f8d3 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Sun, 5 Feb 2023 13:08:31 +0100 Subject: [PATCH 17/25] Update constants for `Receipt` --- EIPS/eip-6404.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 9fc851e4067419..644806fe480174 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -569,13 +569,12 @@ A new `Receipt` SSZ container is introduced to represent receipts. | - | - | | `Topic` | `Bytes32` | -| Name | Value | -| - | - | -| [`BYTES_PER_LOGS_BLOOM`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#execution) | `uint64(2**8)` (= 256) | -| `MAX_TOPICS_PER_LOG` | `uint64(2**24)` (= 16,777,216) | -| `MAX_LOG_DATA_SIZE` | `uint64(2**24)` (= 16,777,216) | -| `MAX_LOGS_PER_RECEIPT` | `uint64(2**24)` (= 16,777,216) | -| `MAX_RECEIPTS_PER_PAYLOAD` | `uint64(2**20)` (= 1,048,576) | Maximum amount of receipts allowed in each block | +| Name | Value | Description | +| - | - | - | +| [`BYTES_PER_LOGS_BLOOM`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#execution) | `uint64(2**8)` (= 256) | Fixed constant | +| `MAX_TOPICS_PER_LOG` | `uint64(2**2)` (= 4) | `LOG0` through `LOG4` opcodes allow 0-4 topics per log | +| `MAX_LOG_DATA_SIZE` | `uint64(2**24)` (= 16,777,216) | Recommended devp2p soft limit for entire receipt: [2 MiB](https://github.com/ethereum/devp2p/blob/bd17dac4228c69b6379644355f373669f74952cd/caps/eth.md#receipts-0x10) | +| `MAX_LOGS_PER_RECEIPT` | `uint64(2**24)` (= 16,777,216) | Recommended devp2p soft limit for entire receipt: [2 MiB](https://github.com/ethereum/devp2p/blob/bd17dac4228c69b6379644355f373669f74952cd/caps/eth.md#receipts-0x10) | ```python class ReceiptLog(Container): From dd620dd63fea6e1132d89a55c525fea6d8619649 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Sun, 5 Feb 2023 13:22:43 +0100 Subject: [PATCH 18/25] Update `MAX_LOGS_PER_RECEIPT` --- EIPS/eip-6404.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 644806fe480174..8760dfaef5d7f5 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -574,7 +574,7 @@ A new `Receipt` SSZ container is introduced to represent receipts. | [`BYTES_PER_LOGS_BLOOM`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#execution) | `uint64(2**8)` (= 256) | Fixed constant | | `MAX_TOPICS_PER_LOG` | `uint64(2**2)` (= 4) | `LOG0` through `LOG4` opcodes allow 0-4 topics per log | | `MAX_LOG_DATA_SIZE` | `uint64(2**24)` (= 16,777,216) | Recommended devp2p soft limit for entire receipt: [2 MiB](https://github.com/ethereum/devp2p/blob/bd17dac4228c69b6379644355f373669f74952cd/caps/eth.md#receipts-0x10) | -| `MAX_LOGS_PER_RECEIPT` | `uint64(2**24)` (= 16,777,216) | Recommended devp2p soft limit for entire receipt: [2 MiB](https://github.com/ethereum/devp2p/blob/bd17dac4228c69b6379644355f373669f74952cd/caps/eth.md#receipts-0x10) | +| `MAX_LOGS_PER_RECEIPT` | `uint64(2**20)` (= 1,048,576) | Same scaling factor as [`MAX_TRANSACTIONS_PER_PAYLOAD`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#execution) | ```python class ReceiptLog(Container): From b65dc3d887be018012a24e503213ea48d98ac0bd Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Sun, 5 Feb 2023 15:34:43 +0100 Subject: [PATCH 19/25] Add rationale about `ReceiptLog` data. --- EIPS/eip-6404.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 8760dfaef5d7f5..775903446f8a59 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -774,6 +774,10 @@ The perpetual transaction hash is used by many applications to uniquely identify For example, when requesting the `Receipt` for a given transaction hash, the response could include (1) the `Receipt` itself, (2) the index of the `Receipt` within `payload.receipts`, (3) the SSZ merkle proof that the `Receipt` is indeed located at that index, (4) the SSZ merkle proof that the transaction with the same index actually has the requested perpetual transaction hash. +### What about `ReceiptLog` data? + +`ReceiptLog` data is formatted according to the Ethereum contract ABI. Merkleizing log data according to its original structure would be more useful than merkleizing it as a `ByteVector`. However, the data structure is determined by the log event signature, of which only the hash is known. As the hash preimages are erased from emitted EVM logs, it is not reliably possible to recover the original log event signature. Therefore, log data is provided as a `ByteVector` for now, with the option for a future EIP to extend it. + ## Backwards Compatibility Applications that solely rely on the legacy RLP encoding but do not rely on the MPT commitment in the block header can still be used through a re-encoding proxy. From f46baeee989caa112e21c7f5c026059d38388b7c Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Sun, 5 Feb 2023 15:35:47 +0100 Subject: [PATCH 20/25] Remove merkle proof length from rationale --- EIPS/eip-6404.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 775903446f8a59..0ae9a751da18b4 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -762,8 +762,6 @@ def decode_withdrawal(encoded_withdrawal: bytes) -> Withdrawal: - **Static merkle tree shape:** Compared to approaches based on SSZ `Union`, it is not necessary to branch on `hash_version` to determine the `GeneralizedIndex` for common fields. For example, a proof for a `Transaction`'s `value` field always has the exact same structure. -- **Shorter merkle proofs:** Compared to approaches based on SSZ `Onion` (merkleized as \[o]bject, serialized as u\[nion]), there is less nesting in the merkle tree. This makes proofs shorter (32 bytes per nesting level) - - **Easy to deprecate legacy transactions:** If a transaction type is no longer allowed, the tree shape remains static. In non-archive settings, the `decode_signed_transaction`, `get_transaction_hash` and `get_transaction_sighash` functions can be adjusted to drop support for legacy transaction types. - **Prior art:** Multiple modules of Ethereum already process common fields in a unified way. The consensus pytests use `is_post_fork` to conditionally enable logic. The execution JSON-RPC reports transaction fields under the same name regardless of type. The consensus light client protocol incorporates a very similar mechanism for upgrading consensus `ExecutionPayloadHeader` to later formats: [`get_transaction_sighash` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/sync-protocol.md#modified-get_lc_execution_root) / [`upgrade_to_latest` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/full-node.md#modified-block_to_light_client_header). From ff2b2f927d55683f5261cb8df310be7e72f50335 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Mon, 6 Feb 2023 16:39:12 +0100 Subject: [PATCH 21/25] rm `TxHashVersion`, rm EIP-155 type, rm extra tree --- EIPS/eip-6404.md | 598 ++++++++++++++++++++++++----------------------- 1 file changed, 303 insertions(+), 295 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 0ae9a751da18b4..07fe54742db0af 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -43,34 +43,39 @@ A new [SSZ type](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562 ### Transactions -This design proposes a single normalized `Transaction` type. To include a transaction from an earlier type into a block, it needs to be migrated using `decode_signed_transaction`. For the purpose of signature validation, the originally signed transaction hash can be recomputed using `get_transaction_sighash`. For applications that need a perpetual identifier, `get_transaction_hash` can be used. +This design proposes a single normalized `Transaction` type. Transactions are converted using `decode_signed_transaction` for inclusion in the consensus `ExecutionPayload`. -#### Hash version +#### [EIP-2718](./eip-2718.md) transaction types + +The value `0x00` is marked as a reserved [EIP-2718](./eip-2718.md) transaction type. -For the purpose of transaction signature validation, it is necessary to recover the originally signed transaction hash. A new enumeration is introduced to indicate the original transaction hashing method. Note that this is different from the [EIP-2718](./eip-2718.md) transaction type, as legacy transactions may have been signed using different methods. The [EIP-2718](./eip-2718.md) transaction type is still used to encapsule different transaction formats on the network. +- `0x00` represents an [EIP-2718](./eip-2718.md) `LegacyTransaction` in SSZ. Note that this is already similarly used in the execution JSON-RPC API. | Name | SSZ equivalent | Description | | - | - | - | -| `TxHashVersion` | `uint8` | Original hashing method used for signing the transaction +| `TransactionType` | `uint8` | [EIP-2718](./eip-2718.md) transaction type, range `[0x00, 0x7F]` | | Name | Value | Description | | - | - | - | -| `TX_HASH_VERSION_LEGACY` | `TxHashVersion(0x03)` | Replayable [`LegacyTransaction`](./eip-2718.md#transactions) | -| `TX_HASH_VERSION_EIP155` | `TxHashVersion(0x04)` | [EIP-155](./eip-155.md) `LegacyTransaction` | -| `TX_HASH_VERSION_EIP2930` | `TxHashVersion(0x01)` | [EIP-2930](./eip-2930.md#definitions) transaction | -| `TX_HASH_VERSION_EIP1559` | `TxHashVersion(0x02)` | [EIP-1559](./eip-1559.md#specification) transaction | -| `TX_HASH_VERSION_EIP4844` | `TxHashVersion(0x05)` | [EIP-4844](./eip-4844.md#parameters) transaction | +| `TRANSACTION_TYPE_LEGACY` | `TransactionType(0x00)` | [`LegacyTransaction`](./eip-2718.md#transactions) (only used in SSZ) | +| `TRANSACTION_TYPE_EIP2930` | `TransactionType(0x01)` | [EIP-2930](./eip-2930.md#definitions) transaction | +| `TRANSACTION_TYPE_EIP1559` | `TransactionType(0x02)` | [EIP-1559](./eip-1559.md#specification) transaction | +| `TRANSACTION_TYPE_EIP4844` | `TransactionType(0x05)` | [EIP-4844](./eip-4844.md#parameters) transaction | -#### [EIP-2718](./eip-2718.md) transaction types +For transactions with `TRANSACTION_TYPE_LEGACY`, post-[EIP-155](./eip-155.md) legacy transactions with chain ID encoded into the `v` value of the signature need to be distinguishable from `LegacyTransaction` that lack chain ID. For that purpose, a `TransactionSubtype`is defined. -The values `0x00`, `0x03`, and `0x04` are marked as reserved [EIP-2718](./eip-2718.md) transaction types. +| Name | SSZ equivalent | Description | +| - | - | - | +| `TransactionSubtype` | `uint8` | Transaction subtype | -- `0x00` is reserved to avoid ambiguity with `type` in the execution JSON-RPC API, where `0x00` denotes a `LegacyTransaction` with chain ID encoded into the `v` value of the signature according to [EIP-155](./eip-155.md). Transactions of type `0x03` and `0x04` may be represented as `0x00` for backwards compatibility reasons. -- `0x03` and `0x04` are reserved to avoid ambiguity with `TX_HASH_VERSION_LEGACY` and `TX_HASH_VERSION_EIP155`. +| Name | Value | Description | +| - | - | - | +| `TRANSACTION_SUBTYPE_POST_EIP155` | `TransactionSubtype(0x00)` | Post [EIP-155](./eip-155.md) transaction | +| `TRANSACTION_SUBTYPE_LEGACY` | `TransactionSubtype(0x01)` | `LegacyTransaction` without chain ID | #### Consensus `ExecutionPayload` changes (Transactions) -The existing [consensus `Transaction`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#custom-types) container represents transactions as opaque, serialized [`EIP-2718`](./eip-2718.md) typed transactions. Once this EIP activates, this definition is replaced with a new `SignedTransaction` SSZ container. +The existing [consensus `Transaction`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#custom-types) container represents transactions as opaque, serialized [`EIP-2718`](./eip-2718.md) typed transactions. This definition is replaced with a new SSZ container. | Name | SSZ equivalent | | - | - | @@ -103,15 +108,21 @@ class ECDSASignature(Container): r: uint256 s: uint256 +class TypedTransaction(Container): + tx_type: TransactionType + tx_subtype: TransactionSubtype + payload: Transaction + class SignedTransaction(Container): - hash_version: TxHashVersion - tx: Transaction + tx: TypedTransaction signature: ECDSASignature -``` -The [consensus `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) is updated to use the new `SignedTransaction` SSZ container. +class IndexedTransaction(Container): + signed_tx: SignedTransaction + tx_hash: Hash32 +``` -Furthermore, it is extended with a new `transaction_hashes_root` field to commit to the perpetual transaction hashes within the block. +The [consensus `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) is updated to use the new `IndexedTransaction` SSZ container. | Name | Value | Description | | - | - | - | @@ -120,176 +131,197 @@ Furthermore, it is extended with a new `transaction_hashes_root` field to commit ```python class ExecutionPayload(Container): ... - transactions: List[SignedTransaction, MAX_TRANSACTIONS_PER_PAYLOAD] - ... - transaction_hashes_root: Root -``` - -To compute the new `transaction_hashes_root`, the list of the transaction hashes of individual `SignedTransaction` containers is represented as a SSZ `List`. - -```python -payload.transaction_hashes_root = hash_tree_root(List[Hash32, MAX_TRANSACTIONS_PER_PAYLOAD]( - get_transaction_hash(transaction_0), - get_transaction_hash(transaction_1), - get_transaction_hash(transaction_2), + transactions: List[IndexedTransaction, MAX_TRANSACTIONS_PER_PAYLOAD] ... -)) ``` #### Consensus `ExecutionPayloadHeader` changes (Transactions) The [consensus `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader) is updated for the new `ExecutionPayload.transactions` definition. -Likewise, it is extended with a new `transaction_hashes_root` field matching the new `ExecutionPayload.transaction_hashes_root` definition. - ```python payload_header.transactions_root = hash_tree_root(payload.transactions) -payload_header.transaction_hashes_root = payload.transaction_hashes_root ``` #### Execution block header changes (Transactions) The [execution block header's `txs-root`](https://github.com/ethereum/devp2p/blob/bd17dac4228c69b6379644355f373669f74952cd/caps/eth.md#block-encoding-and-validity) is updated to match the consensus `ExecutionPayloadHeader.transactions_root`. -Furthermore, the execution block header is extended with a new `txs-hashes-root` field matching the `ExecutionPayloadHeader.transaction_hashes_root` definition. - #### Transaction helpers These helpers use `BlobTransaction` and `SignedBlobTransaction` as defined in [EIP-4844](./eip-4844.md). ```python -def get_transaction_sighash(tx: Transaction, hash_version: TxHashVersion) -> bytes: - if hash_version == TX_HASH_VERSION_EIP4844: +def validate_transaction(tx: TypedTransaction): + if tx.tx_type != TRANSACTION_TYPE_LEGACY: + assert tx.tx_subtype == TRANSACTION_SUBTYPE_POST_EIP155 + + if tx.tx_type == TRANSACTION_TYPE_EIP4844: + return + assert tx.payload.max_fee_per_data_gas == 0 + assert len(tx.payload.blob_versioned_hashes) == 0 + + if tx.tx_type == TRANSACTION_TYPE_EIP1559: + return + assert tx.payload.max_priority_fee_per_gas == tx.payload.max_fee_per_gas + + if tx.tx_type == TRANSACTION_TYPE_EIP2930: + return + assert len(tx.payload.access_list) == 0 + + if tx.tx_type == TRANSACTION_TYPE_LEGACY: + if tx.tx_subtype == TRANSACTION_SUBTYPE_POST_EIP155: + pass + else: + assert tx.tx_subtype == TRANSACTION_SUBTYPE_LEGACY + assert tx.payload.chain_id == uint64(0) + return + assert False +``` + +```python +def compute_transaction_sighash(tx: TypedTransaction) -> bytes: + if tx.tx_type != TRANSACTION_TYPE_LEGACY: + assert tx.tx_subtype == TRANSACTION_SUBTYPE_POST_EIP155 + + if tx.tx_type == TRANSACTION_TYPE_EIP4844: return keccak([0x05] + SSZ.encode(BlobTransaction( - chain_id=tx.chain_id, - nonce=tx.nonce, - max_priority_fee_per_gas=tx.max_priority_fee_per_gas, - max_fee_per_gas=tx.max_fee_per_gas, - gas=tx.gas_limit, - to=tx.to, - value=tx.value, - data=tx.data, - access_list=tx.access_list, - max_fee_per_data_gas=tx.max_fee_per_data_gas, - blob_versioned_hashes=tx.blob_versioned_hashes, + chain_id=tx.payload.chain_id, + nonce=tx.payload.nonce, + max_priority_fee_per_gas=tx.payload.max_priority_fee_per_gas, + max_fee_per_gas=tx.payload.max_fee_per_gas, + gas=tx.payload.gas_limit, + to=tx.payload.to, + value=tx.payload.value, + data=tx.payload.data, + access_list=tx.payload.access_list, + max_fee_per_data_gas=tx.payload.max_fee_per_data_gas, + blob_versioned_hashes=tx.payload.blob_versioned_hashes, ))) - assert tx.max_fee_per_data_gas == 0 - assert len(tx.blob_versioned_hashes) == 0 + assert tx.payload.max_fee_per_data_gas == 0 + assert len(tx.payload.blob_versioned_hashes) == 0 - if hash_version == TX_HASH_VERSION_EIP1559: + if tx.tx_type == TRANSACTION_TYPE_EIP1559: schema = ( - (big_endian_int, tx.chain_id), - (big_endian_int, tx.nonce), - (big_endian_int, tx.max_priority_fee_per_gas), - (big_endian_int, tx.max_fee_per_gas), - (big_endian_int, tx.gas_limit), - (binary, tx.to if tx.to is not None else []), - (big_endian_int, tx.value), - (binary, tx.data), + (big_endian_int, tx.payload.chain_id), + (big_endian_int, tx.payload.nonce), + (big_endian_int, tx.payload.max_priority_fee_per_gas), + (big_endian_int, tx.payload.max_fee_per_gas), + (big_endian_int, tx.payload.gas_limit), + (binary, tx.payload.to if tx.payload.to is not None else []), + (big_endian_int, tx.payload.value), + (binary, tx.payload.data), (List([Binary[20, 20], List([Binary[32, 32]])]), [ - (access_tuple.address, access_tuple.storage_keys) for access_tuple in tx.access_list + ( + access_tuple.address, + access_tuple.storage_keys, + ) for access_tuple in tx.payload.access_list ]), ) sedes = List([schema for schema, _ in schema]) values = [value for _, value in schema] - return keccak([0x02] + rlp.encode(values, sedes)) - assert tx.max_priority_fee_per_gas == tx.max_fee_per_gas + assert tx.payload.max_priority_fee_per_gas == tx.payload.max_fee_per_gas - if hash_version == TX_HASH_VERSION_EIP2930: + if tx.tx_type == TRANSACTION_TYPE_EIP2930: schema = ( - (big_endian_int, tx.chain_id), - (big_endian_int, tx.nonce), - (big_endian_int, tx.max_fee_per_gas), - (big_endian_int, tx.gas_limit), - (binary, tx.to if tx.to is not None else []), - (big_endian_int, tx.value), - (binary, tx.data), + (big_endian_int, tx.payload.chain_id), + (big_endian_int, tx.payload.nonce), + (big_endian_int, tx.payload.max_fee_per_gas), + (big_endian_int, tx.payload.gas_limit), + (binary, tx.payload.to if tx.payload.to is not None else []), + (big_endian_int, tx.payload.value), + (binary, tx.payload.data), (List([Binary[20, 20], List([Binary[32, 32]])]), [ - (access_tuple.address, access_tuple.storage_keys) for access_tuple in tx.access_list + ( + access_tuple.address, + access_tuple.storage_keys, + ) for access_tuple in tx.payload.access_list ]), ) sedes = List([schema for schema, _ in schema]) values = [value for _, value in schema] - return keccak([0x01] + rlp.encode(values, sedes)) - assert len(tx.access_list) == 0 - - if hash_version == TX_HASH_VERSION_EIP155: - schema = ( - (big_endian_int, tx.nonce), - (big_endian_int, tx.max_fee_per_gas), - (big_endian_int, tx.gas_limit), - (binary, tx.to if tx.to is not None else []), - (big_endian_int, tx.value), - (binary, tx.data), - (big_endian_int, tx.chain_id), - (big_endian_int, 0), - (big_endian_int, 0), - ) - sedes = List([schema for schema, _ in schema]) - values = [value for _, value in schema] - - return keccak(rlp.encode(values, sedes)) - - assert tx.chain_id == 0 - - if hash_version == TX_HASH_VERSION_LEGACY: - schema = ( - (big_endian_int, tx.nonce), - (big_endian_int, tx.max_fee_per_gas), - (big_endian_int, tx.gas_limit), - (binary, tx.to if tx.to is not None else []), - (big_endian_int, tx.value), - (binary, tx.data), - ) - sedes = List([schema for schema, _ in schema]) - values = [value for _, value in schema] - - return keccak(rlp.encode(values, sedes)) + assert len(tx.payload.access_list) == 0 + + if tx.tx_type == TRANSACTION_TYPE_LEGACY: + if tx.tx_subtype == TRANSACTION_SUBTYPE_POST_EIP155: + schema = ( + (big_endian_int, tx.payload.nonce), + (big_endian_int, tx.payload.max_fee_per_gas), + (big_endian_int, tx.payload.gas_limit), + (binary, tx.payload.to if tx.payload.to is not None else []), + (big_endian_int, tx.payload.value), + (binary, tx.payload.data), + (big_endian_int, tx.payload.chain_id), + (big_endian_int, 0), + (big_endian_int, 0), + ) + sedes = List([schema for schema, _ in schema]) + values = [value for _, value in schema] + return keccak(rlp.encode(values, sedes)) + else: + assert tx.tx_subtype == TRANSACTION_SUBTYPE_LEGACY + assert tx.payload.chain_id == uint64(0) + schema = ( + (big_endian_int, tx.payload.nonce), + (big_endian_int, tx.payload.max_fee_per_gas), + (big_endian_int, tx.payload.gas_limit), + (binary, tx.payload.to if tx.payload.to is not None else []), + (big_endian_int, tx.payload.value), + (binary, tx.payload.data), + ) + sedes = List([schema for schema, _ in schema]) + values = [value for _, value in schema] + return keccak(rlp.encode(values, sedes)) assert False ``` ```python def encode_signed_transaction(signed_tx: SignedTransaction) -> bytes: - hash_version = signed_tx.hash_version + tx = signed_tx.tx + if tx.tx_type != TRANSACTION_TYPE_LEGACY: + assert tx.tx_subtype == TRANSACTION_SUBTYPE_POST_EIP155 - if hash_version == TX_HASH_VERSION_EIP4844: + if tx.tx_type == TRANSACTION_TYPE_EIP4844: return [0x05] + SSZ.encode(SignedBlobTransaction( message=BlobTransaction( - chain_id=signed_tx.tx.chain_id, - nonce=signed_tx.tx.nonce, - max_priority_fee_per_gas=signed_tx.tx.max_priority_fee_per_gas, - max_fee_per_gas=signed_tx.tx.max_fee_per_gas, - gas=signed_tx.tx.gas_limit, - to=signed_tx.tx.to, - value=signed_tx.tx.value, - data=signed_tx.tx.data, - access_list=signed_tx.tx.access_list, - max_fee_per_data_gas=signed_tx.tx.max_fee_per_data_gas, - blob_versioned_hashes=signed_tx.tx.blob_versioned_hashes, + chain_id=tx.payload.chain_id, + nonce=tx.payload.nonce, + max_priority_fee_per_gas=tx.payload.max_priority_fee_per_gas, + max_fee_per_gas=tx.payload.max_fee_per_gas, + gas=tx.payload.gas_limit, + to=tx.payload.to, + value=tx.payload.value, + data=tx.payload.data, + access_list=tx.payload.access_list, + max_fee_per_data_gas=tx.payload.max_fee_per_data_gas, + blob_versioned_hashes=tx.payload.blob_versioned_hashes, signature=signed_tx.signature, ))) - assert tx.max_fee_per_data_gas == 0 - assert len(tx.blob_versioned_hashes) == 0 + assert tx.payload.max_fee_per_data_gas == 0 + assert len(tx.payload.blob_versioned_hashes) == 0 - if hash_version == TX_HASH_VERSION_EIP1559: + if tx.tx_type == TRANSACTION_TYPE_EIP1559: schema = ( - (big_endian_int, tx.chain_id), - (big_endian_int, tx.nonce), - (big_endian_int, tx.max_priority_fee_per_gas), - (big_endian_int, tx.max_fee_per_gas), - (big_endian_int, tx.gas_limit), - (binary, tx.to if tx.to is not None else []), - (big_endian_int, tx.value), - (binary, tx.data), + (big_endian_int, tx.payload.chain_id), + (big_endian_int, tx.payload.nonce), + (big_endian_int, tx.payload.max_priority_fee_per_gas), + (big_endian_int, tx.payload.max_fee_per_gas), + (big_endian_int, tx.payload.gas_limit), + (binary, tx.payload.to if tx.payload.to is not None else []), + (big_endian_int, tx.payload.value), + (binary, tx.payload.data), (List([Binary[20, 20], List([Binary[32, 32]])]), [ - (access_tuple.address, access_tuple.storage_keys) for access_tuple in tx.access_list + ( + access_tuple.address, + access_tuple.storage_keys, + ) for access_tuple in tx.payload.access_list ]), (big_endian_int, 1 if signed_tx.signature.y_parity else 0), (big_endian_int, signed_tx.signature.r), @@ -297,22 +329,24 @@ def encode_signed_transaction(signed_tx: SignedTransaction) -> bytes: ) sedes = List([schema for schema, _ in schema]) values = [value for _, value in schema] - return [0x02] + rlp.encode(values, sedes) - assert tx.max_priority_fee_per_gas == tx.max_fee_per_gas + assert tx.payload.max_priority_fee_per_gas == tx.payload.max_fee_per_gas - if hash_version == TX_HASH_VERSION_EIP2930: + if tx.tx_type == TRANSACTION_TYPE_EIP2930: schema = ( - (big_endian_int, tx.chain_id), - (big_endian_int, tx.nonce), - (big_endian_int, tx.max_fee_per_gas), - (big_endian_int, tx.gas_limit), - (binary, tx.to if tx.to is not None else []), - (big_endian_int, tx.value), - (binary, tx.data), + (big_endian_int, tx.payload.chain_id), + (big_endian_int, tx.payload.nonce), + (big_endian_int, tx.payload.max_fee_per_gas), + (big_endian_int, tx.payload.gas_limit), + (binary, tx.payload.to if tx.payload.to is not None else []), + (big_endian_int, tx.payload.value), + (binary, tx.payload.data), (List([Binary[20, 20], List([Binary[32, 32]])]), [ - (access_tuple.address, access_tuple.storage_keys) for access_tuple in tx.access_list + ( + access_tuple.address, + access_tuple.storage_keys, + ) for access_tuple in tx.payload.access_list ]), (big_endian_int, 1 if signed_tx.signature.y_parity else 0), (big_endian_int, signed_tx.signature.r), @@ -320,107 +354,68 @@ def encode_signed_transaction(signed_tx: SignedTransaction) -> bytes: ) sedes = List([schema for schema, _ in schema]) values = [value for _, value in schema] - return [0x01] + rlp.encode(values, sedes) - assert len(tx.access_list) == 0 - - if hash_version == TX_HASH_VERSION_EIP155: - schema = ( - (big_endian_int, tx.nonce), - (big_endian_int, tx.max_fee_per_gas), - (big_endian_int, tx.gas_limit), - (binary, tx.to if tx.to is not None else []), - (big_endian_int, tx.value), - (binary, tx.data), - (big_endian_int, (1 if signed_tx.signature.y_parity else 0) + tx.chain_id * 2 + 35), - (big_endian_int, signed_tx.signature.r), - (big_endian_int, signed_tx.signature.s), - ) - sedes = List([schema for schema, _ in schema]) - values = [value for _, value in schema] - - return rlp.encode(values, sedes) - - assert tx.chain_id == 0 + assert len(tx.payload.access_list) == 0 - if hash_version == TX_HASH_VERSION_LEGACY: + if tx.tx_type == TRANSACTION_TYPE_LEGACY: + if tx.tx_subtype == TRANSACTION_SUBTYPE_POST_EIP155: + v = (1 if signed_tx.signature.y_parity else 0) + tx.payload.chain_id * 2 + 35 + else: + assert tx.tx_subtype == TRANSACTION_SUBTYPE_LEGACY + assert tx.payload.chain_id == uint64(0) + v = (1 if signed_tx.signature.y_parity else 0) + 27 schema = ( - (big_endian_int, tx.nonce), - (big_endian_int, tx.max_fee_per_gas), - (big_endian_int, tx.gas_limit), - (binary, tx.to if tx.to is not None else []), - (big_endian_int, tx.value), - (binary, tx.data), - (big_endian_int, (1 if signed_tx.signature.y_parity else 0) + 27), + (big_endian_int, tx.payload.nonce), + (big_endian_int, tx.payload.max_fee_per_gas), + (big_endian_int, tx.payload.gas_limit), + (binary, tx.payload.to if tx.payload.to is not None else []), + (big_endian_int, tx.payload.value), + (binary, tx.payload.data), + (big_endian_int, v), (big_endian_int, signed_tx.signature.r), (big_endian_int, signed_tx.signature.s), ) sedes = List([schema for schema, _ in schema]) values = [value for _, value in schema] - return rlp.encode(values, sedes) assert False ``` ```python -def get_transaction_hash(signed_tx: SignedTransaction) -> bytes: +def compute_transaction_hash(signed_tx: SignedTransaction) -> bytes: return keccak(encode_signed_transaction(signed_tx)) ``` -```python -def validate_transaction(signed_tx: SignedTransaction): - hash_version = signed_tx.hash_version - - if hash_version == TX_HASH_VERSION_EIP4844: - return - assert signed_tx.tx.max_fee_per_data_gas == 0 - assert len(signed_tx.tx.blob_versioned_hashes) == 0 - - if hash_version == TX_HASH_VERSION_EIP1559: - return - assert signed_tx.tx.max_priority_fee_per_gas == signed_tx.tx.max_fee_per_gas - - if hash_version == TX_HASH_VERSION_EIP2930: - return - assert len(signed_tx.tx.access_list) == 0 - - if hash_version == TX_HASH_VERSION_EIP155: - return - assert signed_tx.tx.chain_id == 0 - - if hash_Version == TX_HASH_VERSION_LEGACY: - return - assert False -``` - ```python def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: - tx_type = encoded_signed_tx[0] # EIP-2718 transaction type + eip2718_type = encoded_signed_tx[0] - if tx_type == 0x05: + if eip2718_type == 0x05: pre = SSZ.decode_ssz(SignedBlobTransaction, encoded_signed_tx[1:]) return SignedTransaction( - hash_version=TX_HASH_VERSION_EIP4844, - tx=Transaction( - chain_id=pre.message.chain_id, - nonce=premessage.nonce, - max_priority_fee_per_gas=pre.message.max_priority_fee_per_gas, - max_fee_per_gas=premessage.max_fee_per_gas, - gas_limit=pre.message.gas, - to=pre.message.to, - value=pre.message.value, - data=pre.message.data, - access_list=pre.message.access_list, - max_fee_per_data_gas=pre.message.max_fee_per_data_gas, - blob_versioned_hashes=pre.message.blob_versioned_hashes, + tx=TypedTransaction( + tx_type=TRANSACTION_TYPE_EIP4844, + payload=Transaction( + chain_id=pre.message.chain_id, + nonce=premessage.nonce, + max_priority_fee_per_gas=pre.message.max_priority_fee_per_gas, + max_fee_per_gas=premessage.max_fee_per_gas, + gas_limit=pre.message.gas, + to=pre.message.to, + value=pre.message.value, + data=pre.message.data, + access_list=pre.message.access_list, + max_fee_per_data_gas=pre.message.max_fee_per_data_gas, + blob_versioned_hashes=pre.message.blob_versioned_hashes, + ), ), signature=pre.signature, ) - if tx_type == 0x02: + if eip2718_type == 0x02: class SignedEIP1559Transaction(rlp.Serializable): fields = ( ('chain_id', big_endian_int), @@ -439,20 +434,22 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: pre = SignedEIP1559Transaction.deserialize(encoded_signed_tx[1:]) return SignedTransaction( - hash_version=TX_HASH_VERSION_EIP1559, - tx=Transaction( - chain_id=pre.chain_id, - nonce=pre.nonce, - max_priority_fee_per_gas=pre.max_priority_fee_per_gas, - max_fee_per_gas=pre.max_fee_per_gas, - gas_limit=pre.gas_limit, - to=Address(pre.destination) if len(pre.destination) > 0 else None, - value=pre.amount, - data=pre.data, - access_list=[AccessTuple( - address=access_tuple[0], - storage_keys=access_tuple[1], - ) for access_tuple in pre.access_list], + tx=TypedTransaction( + tx_type=TRANSACTION_TYPE_EIP1559, + payload=Transaction( + chain_id=pre.chain_id, + nonce=pre.nonce, + max_priority_fee_per_gas=pre.max_priority_fee_per_gas, + max_fee_per_gas=pre.max_fee_per_gas, + gas_limit=pre.gas_limit, + to=Address(pre.destination) if len(pre.destination) > 0 else None, + value=pre.amount, + data=pre.data, + access_list=[AccessTuple( + address=access_tuple[0], + storage_keys=access_tuple[1], + ) for access_tuple in pre.access_list], + ), ), signature=ECDSASignature( y_parity=pre.signature_y_parity != 0, @@ -461,7 +458,7 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: ), ) - if tx_type == 0x01: + if eip2718_type == 0x01: class SignedEIP2930Transaction(rlp.Serializable): fields = ( ('chainId', big_endian_int), @@ -479,20 +476,22 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: pre = SignedEIP2930Transaction.deserialize(encoded_signed_tx[1:]) return SignedTransaction( - hash_version=TX_HASH_VERSION_EIP2930, - tx=Transaction( - chain_id=pre.chainId, - nonce=pre.nonce, - max_priority_fee_per_gas=pre.gasPrice, - max_fee_per_gas=pre.gasPrice, - gas_limit=pre.gasLimit, - to=Address(pre.to) if len(pre.to) > 0 else None, - value=pre.value, - data=pre.data, - access_list=[AccessTuple( - address=access_tuple[0], - storage_keys=access_tuple[1], - ) for access_tuple in pre.accessList], + tx=TypedTransaction( + tx_type=TRANSACTION_TYPE_EIP2930, + payload=Transaction( + chain_id=pre.chainId, + nonce=pre.nonce, + max_priority_fee_per_gas=pre.gasPrice, + max_fee_per_gas=pre.gasPrice, + gas_limit=pre.gasLimit, + to=Address(pre.to) if len(pre.to) > 0 else None, + value=pre.value, + data=pre.data, + access_list=[AccessTuple( + address=access_tuple[0], + storage_keys=access_tuple[1], + ) for access_tuple in pre.accessList], + ), ), signature=ECDSASignature( y_parity=pre.signatureYParity != 0, @@ -501,7 +500,7 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: ), ) - if 0xc0 <= tx_type <= 0xfe: + if 0xc0 <= eip2718_type <= 0xfe: class SignedLegacyTransaction(rlp.Serializable): fields = ( ('nonce', big_endian_int), @@ -517,10 +516,20 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: pre = SignedLegacyTransaction.deserialize(encoded_signed_tx) if pre.v not in (27, 28): - return SignedTransaction( - hash_version=TX_HASH_VERSION_EIP155, - tx=Transaction( - chain_id=(pre.v - 35) >> 1, + tx_subtype = TRANSACTION_SUBTYPE_POST_EIP155 + chain_id = (pre.v - 35) >> 1 + y_parity = ((pre.v - 35) & 0x1) != 0 + else: + tx_subtype = TRANSACTION_SUBTYPE_LEGACY + chain_id = 0 + y_parity = ((pre.v - 27) & 0x1) != 0 + + return SignedTransaction( + tx=TypedTransaction( + tx_type=TRANSACTION_TYPE_LEGACY, + tx_subtype=tx_subtype, + payload=Transaction( + chain_id=chain_id, nonce=pre.nonce, max_priority_fee_per_gas=pre.gasprice, max_fee_per_gas=pre.gasprice, @@ -529,26 +538,9 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: value=pre.value, data=pre.data, ), - signature=ECDSASignature( - y_parity=((pre.v - 35) & 0x1) != 0, - r=pre.r, - s=pre.s, - ), - ) - - return SignedTransaction( - hash_version=TX_HASH_VERSION_LEGACY, - tx=Transaction( - nonce=pre.nonce, - max_priority_fee_per_gas=pre.gasprice, - max_fee_per_gas=pre.gasprice, - gas_limit=pre.startgas, - to=Address(pre.to) if len(pre.to) > 0 else None, - value=pre.value, - data=pre.data, ), signature=ECDSASignature( - y_parity=((pre.v - 27) & 0x1) != 0, + y_parity=y_parity, r=pre.r, s=pre.s, ), @@ -559,7 +551,7 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: ### Receipts -Each receipt is linked to the transaction at the same sequential index within a block. The underlying [EIP-2718](./eip-2718.md) transaction type can be inferred from the corresponding transaction. +For the purpose of computing `receipts_root`, receipts are represented with a new SSZ container. #### Consensus `ExecutionPayload` changes (Receipts) @@ -569,7 +561,7 @@ A new `Receipt` SSZ container is introduced to represent receipts. | - | - | | `Topic` | `Bytes32` | -| Name | Value | Description | +| Name | Value | Notes | | - | - | - | | [`BYTES_PER_LOGS_BLOOM`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#execution) | `uint64(2**8)` (= 256) | Fixed constant | | `MAX_TOPICS_PER_LOG` | `uint64(2**2)` (= 4) | `LOG0` through `LOG4` opcodes allow 0-4 topics per log | @@ -587,6 +579,14 @@ class Receipt(Container): cumulative_transaction_gas_used: uint64 logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] logs: List[ReceiptLog, MAX_LOGS_PER_RECEIPT] + +class TypedReceipt(Container): + tx_type: TransactionType + payload: Receipt + +class IndexedReceipt(Container): + receipt: TypedReceipt + tx_hash: Hash32 ``` The [consensus `ExecutionPayload`'s `receipts_root`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) now refers to an SSZ [`Root`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types) instead of an MPT [`Hash32`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types). @@ -598,13 +598,13 @@ class ExecutionPayload(Container): ... ``` -To compute the new `receipts_root`, the list of individual `Receipt` containers is represented as a SSZ `List`. +To compute the new `receipts_root`, the list of individual `IndexedReceipt` containers is represented as a SSZ `List`. ```python -payload.receipts_root = hash_tree_root(List[Receipt, MAX_TRANSACTIONS_PER_PAYLOAD]( - receipt_0, - receipt_1, - receipt_2, +payload.receipts_root = hash_tree_root(List[IndexedReceipt, MAX_TRANSACTIONS_PER_PAYLOAD]( + indexed_receipt_0, + indexed_receipt_1, + indexed_receipt_2, ... )) ``` @@ -631,7 +631,7 @@ The [execution block header's `receipts-root`](https://github.com/ethereum/devp2 #### Receipt helpers ```python -def encode_receipt(receipt: Receipt, hash_version: TxHashVersion) -> bytes: +def encode_receipt(receipt: TypedReceipt) -> bytes: schema = ( (big_endian_int, receipt.status), (big_endian_int, receipt.cumulative_transaction_gas_used), @@ -644,22 +644,20 @@ def encode_receipt(receipt: Receipt, hash_version: TxHashVersion) -> bytes: values = [value for _, value in schema] encoded = rlp.encode(values, sedes) - if hash_version == TX_HASH_VERSION_EIP4844: + if receipt.tx_type == TRANSACTION_TYPE_EIP4844: return [0x05] + encoded - if hash_version == TX_HASH_VERSION_EIP1559: + if receipt.tx_type == TRANSACTION_TYPE_EIP1559: return [0x02] + encoded - if hash_version == TX_HASH_VERSION_EIP2930: + if receipt.tx_type == TRANSACTION_TYPE_EIP2930: return [0x01] + encoded - if hash_version == TX_HASH_VERSION_EIP155: - return encoded - if hash_Version == TX_HASH_VERSION_LEGACY: + if receipt.tx_type == TRANSACTION_TYPE_LEGACY: return encoded assert False ``` ```python def decode_receipt(encoded_receipt: bytes) -> Receipt: - tx_type = encoded_receipt[0] + eip2718_type = encoded_receipt[0] class RLPReceipt(rlp.Serializable): fields = ( @@ -668,13 +666,17 @@ def decode_receipt(encoded_receipt: bytes) -> Receipt: ('logs_bloom', Binary[256, 256]), ('logs', List([Binary[20, 20], List([Binary[32, 32]]), Binary[0, uint64(2**24)]])), ) - if tx_type == 0x05: + if eip2718_type == 0x05: + tx_type = TRANSACTION_TYPE_EIP4844 pre = RLPReceipt.deserialize(encoded_receipt[1:]) - elif tx_type == 0x02: + elif eip2718_type == 0x02: + tx_type = TRANSACTION_TYPE_EIP1559 pre = RLPReceipt.deserialize(encoded_receipt[1:]) - elif tx_type == 0x01: + elif eip2718_type == 0x01: + tx_type = TRANSACTION_TYPE_EIP2930 pre = RLPReceipt.deserialize(encoded_receipt[1:]) - elif 0xc0 <= tx_type <= 0xfe: + elif 0xc0 <= eip2718_type <= 0xfe: + tx_type = TRANSACTION_TYPE_LEGACY pre = RLPReceipt.deserialize(encoded_receipt) else: assert False @@ -760,17 +762,23 @@ def decode_withdrawal(encoded_withdrawal: bytes) -> Withdrawal: - **Superset of all existing transaction types:** The new `Transaction` container supports all existing transaction types. There is no new functionality that was previously disallowed. `Transaction` containers that are created from importing legacy transaction types use default values for fields that were added later. -- **Static merkle tree shape:** Compared to approaches based on SSZ `Union`, it is not necessary to branch on `hash_version` to determine the `GeneralizedIndex` for common fields. For example, a proof for a `Transaction`'s `value` field always has the exact same structure. +- **Static merkle tree shape:** Compared to approaches based on SSZ `Union`, it is not necessary to branch on `tx_type` to determine the `GeneralizedIndex` for common fields. For example, a proof for a `Transaction`'s `value` field always has the exact same structure. + +- **Prior art:** Multiple modules of Ethereum already process common fields in a unified way. The consensus pytests use `is_post_fork` to conditionally enable logic. The execution JSON-RPC reports transaction fields under the same name regardless of type. The consensus light client protocol incorporates a very similar mechanism for upgrading consensus `ExecutionPayloadHeader` to later formats: [`compute_transaction_sighash` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/sync-protocol.md#modified-get_lc_execution_root) / [`upgrade_to_latest` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/full-node.md#modified-block_to_light_client_header). + +### Why `tx_subtype`? + +In [EIP-155](./eip-155.md) there is an ambiguity where a `LegacyTransaction` signature's `v` value in `[27, 28]` denotes absence of `chain_id`, and `[35, 36]` explicitly denotes `chain_id = 0`. -- **Easy to deprecate legacy transactions:** If a transaction type is no longer allowed, the tree shape remains static. In non-archive settings, the `decode_signed_transaction`, `get_transaction_hash` and `get_transaction_sighash` functions can be adjusted to drop support for legacy transaction types. +- For `Transaction` structures, separate transaction types could be introduced to distinguish these cases, but that doesn't work for `Receipt` structures which do not contain the `v` value. This means that the receipt type would be less specific than the transaction type. The `tx_subtype` makes this disparity explicit and allows sharing the main `tx_type` across both `Transaction` and `Receipt`. -- **Prior art:** Multiple modules of Ethereum already process common fields in a unified way. The consensus pytests use `is_post_fork` to conditionally enable logic. The execution JSON-RPC reports transaction fields under the same name regardless of type. The consensus light client protocol incorporates a very similar mechanism for upgrading consensus `ExecutionPayloadHeader` to later formats: [`get_transaction_sighash` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/sync-protocol.md#modified-get_lc_execution_root) / [`upgrade_to_latest` equivalent](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/light-client/full-node.md#modified-block_to_light_client_header). +- Using an `Optional` for the `chain_id` would allow distinguishing the `0` case from the `None` case, but suggests more weight than necessary. In the proposed design, `chain_id` is reported as `0` in both cases, and the extra `tx_subtype` indicates whether a `0` or `None` value is used to compute the transaction signature's `v` value. -### Why the need for `transaction_hashes_root`? +- The execution JSON-RPC API reports the transaction signature's `v` value instead of splitting into `y_parity` and `chain_id`. For the purpose of SSZ merkleization, it is preferrable to not mix up transaction and signature properties. -The perpetual transaction hash is used by many applications to uniquely identify a transaction. The `transaction_hashes_root` allows smart contracts to verify proofs about objects that are linked to the perpetual transaction hash, without having to re-hash the entire transaction according to the original `TxHashVersion`. +### Why `tx_hash`? -For example, when requesting the `Receipt` for a given transaction hash, the response could include (1) the `Receipt` itself, (2) the index of the `Receipt` within `payload.receipts`, (3) the SSZ merkle proof that the `Receipt` is indeed located at that index, (4) the SSZ merkle proof that the transaction with the same index actually has the requested perpetual transaction hash. +The perpetual transaction hash is used by many applications to uniquely identify a transaction. The `tx_hash` allows smart contracts to verify proofs about structures that are linked to the perpetual transaction hash, without having to re-hash the entire transaction according to the original `TransactionType`. ### What about `ReceiptLog` data? @@ -782,7 +790,7 @@ Applications that solely rely on the legacy RLP encoding but do not rely on the Applications that rely on the replaced MPT root commitments in the block header can no longer find that information. Analysis is required whether affected applications have a migration path available to use the SSZ root commitments instead. -- **Transactions:** The perpetual transaction hash is commonly used by block explorers. A helper function `get_transaction_hash` is specified to replicate historic transaction hashes. +- **Transactions:** The perpetual transaction hash is commonly used by block explorers. A helper function `compute_transaction_hash` is specified to replicate historic transaction hashes. - **Receipts:** Receipts are tied to transactions, so are typically tied to either the transaction hash, or to a block hash + sequential transaction index. There is no JSON-RPC endpoint to obtain a receipts proof. It is not expected that major applications rely on the legacy MPT for receipts. From b9955cb7b438378908bf7c34f51dff6311c9abfe Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Wed, 8 Feb 2023 16:52:46 +0100 Subject: [PATCH 22/25] rm `tx_hash` from `Receipt` for concurrent receipt/tx backfill (Geth) --- EIPS/eip-6404.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 07fe54742db0af..76402fa7be4c44 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -583,10 +583,6 @@ class Receipt(Container): class TypedReceipt(Container): tx_type: TransactionType payload: Receipt - -class IndexedReceipt(Container): - receipt: TypedReceipt - tx_hash: Hash32 ``` The [consensus `ExecutionPayload`'s `receipts_root`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) now refers to an SSZ [`Root`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types) instead of an MPT [`Hash32`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types). @@ -598,13 +594,13 @@ class ExecutionPayload(Container): ... ``` -To compute the new `receipts_root`, the list of individual `IndexedReceipt` containers is represented as a SSZ `List`. +To compute the new `receipts_root`, the list of individual `TypedReceipt` containers is represented as a SSZ `List`. ```python -payload.receipts_root = hash_tree_root(List[IndexedReceipt, MAX_TRANSACTIONS_PER_PAYLOAD]( - indexed_receipt_0, - indexed_receipt_1, - indexed_receipt_2, +payload.receipts_root = hash_tree_root(List[TypedReceipt, MAX_TRANSACTIONS_PER_PAYLOAD]( + receipt_0, + receipt_1, + receipt_2, ... )) ``` From 35efb51375590dc58c849fa339d628c173b8f0be Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Wed, 8 Feb 2023 16:53:20 +0100 Subject: [PATCH 23/25] typo (ty @dapplion) --- EIPS/eip-6404.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 76402fa7be4c44..a49b653f68b13a 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -62,7 +62,7 @@ The value `0x00` is marked as a reserved [EIP-2718](./eip-2718.md) transaction t | `TRANSACTION_TYPE_EIP1559` | `TransactionType(0x02)` | [EIP-1559](./eip-1559.md#specification) transaction | | `TRANSACTION_TYPE_EIP4844` | `TransactionType(0x05)` | [EIP-4844](./eip-4844.md#parameters) transaction | -For transactions with `TRANSACTION_TYPE_LEGACY`, post-[EIP-155](./eip-155.md) legacy transactions with chain ID encoded into the `v` value of the signature need to be distinguishable from `LegacyTransaction` that lack chain ID. For that purpose, a `TransactionSubtype`is defined. +For transactions with `TRANSACTION_TYPE_LEGACY`, post-[EIP-155](./eip-155.md) legacy transactions with chain ID encoded into the `v` value of the signature need to be distinguishable from `LegacyTransaction` that lack chain ID. For that purpose, a `TransactionSubtype` is defined. | Name | SSZ equivalent | Description | | - | - | - | From a95a6a5b955c327496fbdc042bf34ea6e64a2dc9 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Wed, 8 Feb 2023 17:24:02 +0100 Subject: [PATCH 24/25] Split away withdrawals (#6465) --- EIPS/eip-6404.md | 85 ++++++------------------------------------------ 1 file changed, 10 insertions(+), 75 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index a49b653f68b13a..7624e31ec3deab 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -1,9 +1,9 @@ --- eip: 6404 -title: SSZ transactions, receipts, and withdrawals -description: Migration of MPT commitments for transactions, receipts, and withdrawals to SSZ +title: SSZ transactions and receipts root +description: Migration of transactions and receipts MPT commitments to SSZ author: Etan Kissling (@etan-status), Vitalik Buterin (@vbuterin) -discussions-to: https://ethereum-magicians.org/t/eip-6404-ssz-transactions-receipts-and-withdrawals/12783 +discussions-to: https://ethereum-magicians.org/t/eip-6404-ssz-transactions-and-receipts-root/12783 status: Draft type: Standards Track category: Core @@ -13,19 +13,19 @@ requires: 155, 658, 1559, 2718, 2930, 4844, 4895 ## Abstract -This EIP defines a migration process of existing Merkle-Patricia Trie (MPT) commitments for transactions, receipts, and withdrawals to SSZ. +This EIP defines a migration process of existing Merkle-Patricia Trie (MPT) commitments for transactions and receipts to SSZ. ## Motivation -While the consensus `ExecutionPayload` and the execution block structure map to each other conceptually, they are encoded differently. This EIP aims to align the encoding of their fields, taking advantage of the more modern SSZ format. This brings several advantages: +While the consensus `ExecutionPayloadHeader` and the execution block header map to each other conceptually, they are encoded differently. This EIP aims to align the encoding of their fields, taking advantage of the more modern SSZ format. This brings several advantages: 1. **Reducing complexity:** Merkle-Patricia Tries (MPT) are hard to work with. Replacing them with SSZ leaves only the state trie in the legacy MPT format. 2. **Better for smart contracts:** The SSZ format is optimized for production and verification of merkle proofs. It allows proving specific fields of containers and allows chunked processing, e.g., to support handling transactions that do not fit into calldata. -3. **Better for light clients:** Light clients with access to the consensus `ExecutionPayload` no longer need to obtain the matching execution block header to verify proofs rooted in `transactions_root` or `withdrawals_root`. Note that the `receipts_root` is currently represented as a MPT root across both execution and consensus. +3. **Better for light clients:** Light clients with access to the consensus `ExecutionPayload` no longer need to obtain the matching execution block header to verify proofs rooted in `transactions_root`. Note that the `receipts_root` is currently represented as a MPT root across both execution and consensus. -4. **Reducing ambiguity:** The names `transactions_root` and `withdrawals_root` are currently used to refer to different roots. The execution block header refers to a MPT root, the consensus `ExecutionPayloadHeader` refers to a SSZ root. +4. **Reducing ambiguity:** The name `transactions_root` is currently used to refer to different roots. The execution block header refers to a MPT root, the consensus `ExecutionPayloadHeader` refers to a SSZ root. ## Specification @@ -689,69 +689,6 @@ def decode_receipt(encoded_receipt: bytes) -> Receipt: ) ``` -### Withdrawals - -#### Execution block header changes (Withdrawals) - -The existing consensus [`Withdrawal`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/capella/beacon-chain.md#withdrawal) SSZ container is used to represent withdrawals. - -```python -class Withdrawal(Container): - index: WithdrawalIndex - validator_index: ValidatorIndex - address: ExecutionAddress - amount: Gwei -``` - -The execution block header's `withdrawals-root` is updated to match the consensus [`ExecutionPayloadHeader.withdrawals_root`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader). - -| Name | Value | Description | -| - | - | - | -| [`MAX_WITHDRAWALS_PER_PAYLOAD`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/capella/beacon-chain.md#execution) | `uint64(2**4)` (= 16) | Maximum amount of withdrawals allowed in each block | - -```python -block_header.withdrawals_root == hash_tree_root(List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD]( - withdrawal_0, - withdrawal_1, - withdrawal_2, - ... -)) -``` - -#### Withdrawal helpers - -```python -def encode_withdrawal(withdrawal: Withdrawal) -> bytes: - schema = ( - (big_endian_int, withdrawal.index), - (big_endian_int, withdrawal.validator_index), - (Binary[20, 20], withdrawal.address), - (big_endian_int, withdrawal.amount), - ) - sedes = List([schema for schema, _ in schema]) - values = [value for _, value in schema] - return rlp.encode(values, sedes) -``` - -```python -def decode_withdrawal(encoded_withdrawal: bytes) -> Withdrawal: - class RLPWithdrawal(rlp.Serializable): - fields = ( - ('index', bid_endian_int), - ('validator_index', big_endian_int), - ('address', Binary[20, 20]), - ('amount', big_endian_int), - ) - pre = RLPWithdrawal.deserialize(encoded_withdrawal) - - return Withdrawal( - index=pre.index, - validator_index=pre.validator_index, - address=pre.address, - amount=pre.amount, - ) -``` - ## Rationale ### Why not multiple `Transaction` containers? @@ -782,15 +719,13 @@ The perpetual transaction hash is used by many applications to uniquely identify ## Backwards Compatibility -Applications that solely rely on the legacy RLP encoding but do not rely on the MPT commitment in the block header can still be used through a re-encoding proxy. +Applications that solely rely on the `TypedTransaction` / `TypedReceipt` RLP encoding but do not rely on the `transactions_root` / `receipts_root` commitment in the block header can still be used through a re-encoding proxy. -Applications that rely on the replaced MPT root commitments in the block header can no longer find that information. Analysis is required whether affected applications have a migration path available to use the SSZ root commitments instead. +Applications that rely on the replaced `transactions_root` / `receipts_root` in the block header can no longer find that information. Analysis is required whether affected applications have a migration path available to use the SSZ root commitments instead. - **Transactions:** The perpetual transaction hash is commonly used by block explorers. A helper function `compute_transaction_hash` is specified to replicate historic transaction hashes. -- **Receipts:** Receipts are tied to transactions, so are typically tied to either the transaction hash, or to a block hash + sequential transaction index. There is no JSON-RPC endpoint to obtain a receipts proof. It is not expected that major applications rely on the legacy MPT for receipts. - -- **Withdrawals:** Withdrawals were introduced in Capella/Shanghai just recently. It is not expected that major applications already rely on the legacy MPT for withdrawals. +- **Receipts:** Receipts are tied to transactions, so are typically tied to either the transaction hash, or to a block hash + sequential transaction index. There is no JSON-RPC endpoint to obtain a receipts proof. It is not expected that major applications rely on the Merkle-Patricia Trie commitment for receipts. ## Test Cases From c354a51041dfa3b15c06d5b3481bb23592a65518 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Wed, 8 Feb 2023 17:52:09 +0100 Subject: [PATCH 25/25] Split away receipts (#6466) --- EIPS/eip-6404.md | 182 +++++------------------------------------------ 1 file changed, 16 insertions(+), 166 deletions(-) diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md index 7624e31ec3deab..766c65559cb722 100644 --- a/EIPS/eip-6404.md +++ b/EIPS/eip-6404.md @@ -1,19 +1,19 @@ --- eip: 6404 -title: SSZ transactions and receipts root -description: Migration of transactions and receipts MPT commitments to SSZ +title: SSZ transactions root +description: Migration of transactions MPT commitment to SSZ author: Etan Kissling (@etan-status), Vitalik Buterin (@vbuterin) -discussions-to: https://ethereum-magicians.org/t/eip-6404-ssz-transactions-and-receipts-root/12783 +discussions-to: https://ethereum-magicians.org/t/eip-6404-ssz-transactions-root/12783 status: Draft type: Standards Track category: Core created: 2023-01-30 -requires: 155, 658, 1559, 2718, 2930, 4844, 4895 +requires: 155, 658, 1559, 2718, 2930, 4844 --- ## Abstract -This EIP defines a migration process of existing Merkle-Patricia Trie (MPT) commitments for transactions and receipts to SSZ. +This EIP defines a migration process of existing Merkle-Patricia Trie (MPT) commitments for transactions to SSZ. ## Motivation @@ -23,7 +23,7 @@ While the consensus `ExecutionPayloadHeader` and the execution block header map 2. **Better for smart contracts:** The SSZ format is optimized for production and verification of merkle proofs. It allows proving specific fields of containers and allows chunked processing, e.g., to support handling transactions that do not fit into calldata. -3. **Better for light clients:** Light clients with access to the consensus `ExecutionPayload` no longer need to obtain the matching execution block header to verify proofs rooted in `transactions_root`. Note that the `receipts_root` is currently represented as a MPT root across both execution and consensus. +3. **Better for light clients:** Light clients with access to the consensus `ExecutionPayload` no longer need to obtain the matching execution block header to verify proofs rooted in `transactions_root`. 4. **Reducing ambiguity:** The name `transactions_root` is currently used to refer to different roots. The execution block header refers to a MPT root, the consensus `ExecutionPayloadHeader` refers to a SSZ root. @@ -41,11 +41,7 @@ A new [SSZ type](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562 - If value is not `None`, serialize as `T`, and merkleize as `List[T, 1]` with length `1`. - Serialize `Optional[T]` as variable-size object (create offset-table entry in enclosing containers). -### Transactions - -This design proposes a single normalized `Transaction` type. Transactions are converted using `decode_signed_transaction` for inclusion in the consensus `ExecutionPayload`. - -#### [EIP-2718](./eip-2718.md) transaction types +### [EIP-2718](./eip-2718.md) transaction types The value `0x00` is marked as a reserved [EIP-2718](./eip-2718.md) transaction type. @@ -73,7 +69,7 @@ For transactions with `TRANSACTION_TYPE_LEGACY`, post-[EIP-155](./eip-155.md) le | `TRANSACTION_SUBTYPE_POST_EIP155` | `TransactionSubtype(0x00)` | Post [EIP-155](./eip-155.md) transaction | | `TRANSACTION_SUBTYPE_LEGACY` | `TransactionSubtype(0x01)` | `LegacyTransaction` without chain ID | -#### Consensus `ExecutionPayload` changes (Transactions) +### Consensus `ExecutionPayload` changes The existing [consensus `Transaction`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#custom-types) container represents transactions as opaque, serialized [`EIP-2718`](./eip-2718.md) typed transactions. This definition is replaced with a new SSZ container. @@ -122,7 +118,7 @@ class IndexedTransaction(Container): tx_hash: Hash32 ``` -The [consensus `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) is updated to use the new `IndexedTransaction` SSZ container. +The [consensus `ExecutionPayload`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/capella/beacon-chain.md#executionpayload) is updated to use the new `IndexedTransaction` SSZ container. | Name | Value | Description | | - | - | - | @@ -135,19 +131,19 @@ class ExecutionPayload(Container): ... ``` -#### Consensus `ExecutionPayloadHeader` changes (Transactions) +### Consensus `ExecutionPayloadHeader` changes -The [consensus `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader) is updated for the new `ExecutionPayload.transactions` definition. +The [consensus `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/capella/beacon-chain.md#executionpayloadheader) is updated for the new `ExecutionPayload.transactions` definition. ```python payload_header.transactions_root = hash_tree_root(payload.transactions) ``` -#### Execution block header changes (Transactions) +### Execution block header changes The [execution block header's `txs-root`](https://github.com/ethereum/devp2p/blob/bd17dac4228c69b6379644355f373669f74952cd/caps/eth.md#block-encoding-and-validity) is updated to match the consensus `ExecutionPayloadHeader.transactions_root`. -#### Transaction helpers +### Helpers These helpers use `BlobTransaction` and `SignedBlobTransaction` as defined in [EIP-4844](./eip-4844.md). @@ -549,146 +545,6 @@ def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: assert False ``` -### Receipts - -For the purpose of computing `receipts_root`, receipts are represented with a new SSZ container. - -#### Consensus `ExecutionPayload` changes (Receipts) - -A new `Receipt` SSZ container is introduced to represent receipts. - -| Name | SSZ equivalent | -| - | - | -| `Topic` | `Bytes32` | - -| Name | Value | Notes | -| - | - | - | -| [`BYTES_PER_LOGS_BLOOM`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#execution) | `uint64(2**8)` (= 256) | Fixed constant | -| `MAX_TOPICS_PER_LOG` | `uint64(2**2)` (= 4) | `LOG0` through `LOG4` opcodes allow 0-4 topics per log | -| `MAX_LOG_DATA_SIZE` | `uint64(2**24)` (= 16,777,216) | Recommended devp2p soft limit for entire receipt: [2 MiB](https://github.com/ethereum/devp2p/blob/bd17dac4228c69b6379644355f373669f74952cd/caps/eth.md#receipts-0x10) | -| `MAX_LOGS_PER_RECEIPT` | `uint64(2**20)` (= 1,048,576) | Same scaling factor as [`MAX_TRANSACTIONS_PER_PAYLOAD`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#execution) | - -```python -class ReceiptLog(Container): - address: Address - topics: List[Topic, MAX_TOPICS_PER_LOG] - data: ByteVector[MAX_LOG_DATA_SIZE] - -class Receipt(Container): - status: uint256 # EIP-658 - cumulative_transaction_gas_used: uint64 - logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] - logs: List[ReceiptLog, MAX_LOGS_PER_RECEIPT] - -class TypedReceipt(Container): - tx_type: TransactionType - payload: Receipt -``` - -The [consensus `ExecutionPayload`'s `receipts_root`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayload) now refers to an SSZ [`Root`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types) instead of an MPT [`Hash32`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types). - -```python -class ExecutionPayload(Container): - ... - receipts_root: Root - ... -``` - -To compute the new `receipts_root`, the list of individual `TypedReceipt` containers is represented as a SSZ `List`. - -```python -payload.receipts_root = hash_tree_root(List[TypedReceipt, MAX_TRANSACTIONS_PER_PAYLOAD]( - receipt_0, - receipt_1, - receipt_2, - ... -)) -``` - -#### Consensus `ExecutionPayloadHeader` changes (Receipts) - -The [consensus `ExecutionPayloadHeader`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/eip4844/beacon-chain.md#executionpayloadheader) is updated for the new `ExecutionPayload.receipts_root` definition. - -```python -class ExecutionPayloadHeader(Container): - ... - receipts_root: Root - ... -``` - -```python -payload_header.receipts_root = payload.receipts_root -``` - -#### Execution block header changes (Receipts) - -The [execution block header's `receipts-root`](https://github.com/ethereum/devp2p/blob/bd17dac4228c69b6379644355f373669f74952cd/caps/eth.md#block-encoding-and-validity) is updated to match the consensus `ExecutionPayloadHeader.receipts_root`. - -#### Receipt helpers - -```python -def encode_receipt(receipt: TypedReceipt) -> bytes: - schema = ( - (big_endian_int, receipt.status), - (big_endian_int, receipt.cumulative_transaction_gas_used), - (Binary[256, 256], receipt.logs_bloom), - (List([Binary[20, 20], List([Binary[32, 32]]), Binary[0, uint64(2**24)]]), [ - (log.address, log.topics, log.data) for log in receipt.logs - ]), - ) - sedes = List([schema for schema, _ in schema]) - values = [value for _, value in schema] - encoded = rlp.encode(values, sedes) - - if receipt.tx_type == TRANSACTION_TYPE_EIP4844: - return [0x05] + encoded - if receipt.tx_type == TRANSACTION_TYPE_EIP1559: - return [0x02] + encoded - if receipt.tx_type == TRANSACTION_TYPE_EIP2930: - return [0x01] + encoded - if receipt.tx_type == TRANSACTION_TYPE_LEGACY: - return encoded - assert False -``` - -```python -def decode_receipt(encoded_receipt: bytes) -> Receipt: - eip2718_type = encoded_receipt[0] - - class RLPReceipt(rlp.Serializable): - fields = ( - ('status', big_endian_int), - ('cumulative_transaction_gas_used', big_endian_int), - ('logs_bloom', Binary[256, 256]), - ('logs', List([Binary[20, 20], List([Binary[32, 32]]), Binary[0, uint64(2**24)]])), - ) - if eip2718_type == 0x05: - tx_type = TRANSACTION_TYPE_EIP4844 - pre = RLPReceipt.deserialize(encoded_receipt[1:]) - elif eip2718_type == 0x02: - tx_type = TRANSACTION_TYPE_EIP1559 - pre = RLPReceipt.deserialize(encoded_receipt[1:]) - elif eip2718_type == 0x01: - tx_type = TRANSACTION_TYPE_EIP2930 - pre = RLPReceipt.deserialize(encoded_receipt[1:]) - elif 0xc0 <= eip2718_type <= 0xfe: - tx_type = TRANSACTION_TYPE_LEGACY - pre = RLPReceipt.deserialize(encoded_receipt) - else: - assert False - - return Receipt( - status=pre.status, - cumulative_transaction_gas_used=pre.cumulative_transaction_gas_used, - logs_bloom=pre.logs_bloom, - logs=[ReceiptLog( - address=log[0], - topics=log[1], - data=log[2], - ) for log in pre.logs], - ) -``` - ## Rationale ### Why not multiple `Transaction` containers? @@ -713,19 +569,13 @@ In [EIP-155](./eip-155.md) there is an ambiguity where a `LegacyTransaction` sig The perpetual transaction hash is used by many applications to uniquely identify a transaction. The `tx_hash` allows smart contracts to verify proofs about structures that are linked to the perpetual transaction hash, without having to re-hash the entire transaction according to the original `TransactionType`. -### What about `ReceiptLog` data? - -`ReceiptLog` data is formatted according to the Ethereum contract ABI. Merkleizing log data according to its original structure would be more useful than merkleizing it as a `ByteVector`. However, the data structure is determined by the log event signature, of which only the hash is known. As the hash preimages are erased from emitted EVM logs, it is not reliably possible to recover the original log event signature. Therefore, log data is provided as a `ByteVector` for now, with the option for a future EIP to extend it. - ## Backwards Compatibility -Applications that solely rely on the `TypedTransaction` / `TypedReceipt` RLP encoding but do not rely on the `transactions_root` / `receipts_root` commitment in the block header can still be used through a re-encoding proxy. - -Applications that rely on the replaced `transactions_root` / `receipts_root` in the block header can no longer find that information. Analysis is required whether affected applications have a migration path available to use the SSZ root commitments instead. +Applications that solely rely on the `TypedTransaction` RLP encoding but do not rely on the `transactions_root` commitment in the block header can still be used through a re-encoding proxy. -- **Transactions:** The perpetual transaction hash is commonly used by block explorers. A helper function `compute_transaction_hash` is specified to replicate historic transaction hashes. +Applications that rely on the replaced `transactions_root` in the block header can no longer find that information. Analysis is required whether affected applications have a migration path available to use the SSZ root commitments instead. -- **Receipts:** Receipts are tied to transactions, so are typically tied to either the transaction hash, or to a block hash + sequential transaction index. There is no JSON-RPC endpoint to obtain a receipts proof. It is not expected that major applications rely on the Merkle-Patricia Trie commitment for receipts. +The perpetual transaction hash is commonly used by block explorers. A helper function `compute_transaction_hash` is specified to replicate historic transaction hashes. ## Test Cases