diff --git a/EIPS/eip-6404.md b/EIPS/eip-6404.md new file mode 100644 index 00000000000000..766c65559cb722 --- /dev/null +++ b/EIPS/eip-6404.md @@ -0,0 +1,594 @@ +--- +eip: 6404 +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-root/12783 +status: Draft +type: Standards Track +category: Core +created: 2023-01-30 +requires: 155, 658, 1559, 2718, 2930, 4844 +--- + +## Abstract + +This EIP defines a migration process of existing Merkle-Patricia Trie (MPT) commitments for transactions to SSZ. + +## Motivation + +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`. + +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 + +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). + +### [EIP-2718](./eip-2718.md) transaction types + +The value `0x00` is marked as a reserved [EIP-2718](./eip-2718.md) transaction type. + +- `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 | +| - | - | - | +| `TransactionType` | `uint8` | [EIP-2718](./eip-2718.md) transaction type, range `[0x00, 0x7F]` | + +| Name | Value | Description | +| - | - | - | +| `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 | + +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 | +| - | - | - | +| `TransactionSubtype` | `uint8` | Transaction subtype | + +| 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 + +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 | +| - | - | +| [`VersionedHash`](./eip-4844.md#type-aliases) | `Bytes32` | + +| Name | Value | +| - | - | +| [`MAX_VERSIONED_HASHES_LIST_SIZE`](./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 + max_fee_per_data_gas: uint256 # EIP-4844 + 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 TypedTransaction(Container): + tx_type: TransactionType + tx_subtype: TransactionSubtype + payload: Transaction + +class SignedTransaction(Container): + tx: TypedTransaction + signature: ECDSASignature + +class IndexedTransaction(Container): + signed_tx: SignedTransaction + tx_hash: Hash32 +``` + +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 | +| - | - | - | +| [`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[IndexedTransaction, MAX_TRANSACTIONS_PER_PAYLOAD] + ... +``` + +### Consensus `ExecutionPayloadHeader` changes + +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 + +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`. + +### Helpers + +These helpers use `BlobTransaction` and `SignedBlobTransaction` as defined in [EIP-4844](./eip-4844.md). + +```python +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.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.payload.max_fee_per_data_gas == 0 + assert len(tx.payload.blob_versioned_hashes) == 0 + + if tx.tx_type == TRANSACTION_TYPE_EIP1559: + schema = ( + (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.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.payload.max_priority_fee_per_gas == tx.payload.max_fee_per_gas + + if tx.tx_type == TRANSACTION_TYPE_EIP2930: + schema = ( + (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.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.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: + tx = signed_tx.tx + if tx.tx_type != TRANSACTION_TYPE_LEGACY: + assert tx.tx_subtype == TRANSACTION_SUBTYPE_POST_EIP155 + + if tx.tx_type == TRANSACTION_TYPE_EIP4844: + return [0x05] + SSZ.encode(SignedBlobTransaction( + message=BlobTransaction( + 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.payload.max_fee_per_data_gas == 0 + assert len(tx.payload.blob_versioned_hashes) == 0 + + if tx.tx_type == TRANSACTION_TYPE_EIP1559: + schema = ( + (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.payload.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 [0x02] + rlp.encode(values, sedes) + + assert tx.payload.max_priority_fee_per_gas == tx.payload.max_fee_per_gas + + if tx.tx_type == TRANSACTION_TYPE_EIP2930: + schema = ( + (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.payload.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 [0x01] + 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: + 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.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 compute_transaction_hash(signed_tx: SignedTransaction) -> bytes: + return keccak(encode_signed_transaction(signed_tx)) +``` + +```python +def decode_signed_transaction(encoded_signed_tx: bytes) -> SignedTransaction: + eip2718_type = encoded_signed_tx[0] + + if eip2718_type == 0x05: + pre = SSZ.decode_ssz(SignedBlobTransaction, encoded_signed_tx[1:]) + + return SignedTransaction( + 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 eip2718_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(encoded_signed_tx[1:]) + + return SignedTransaction( + 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, + r=pre.signature_r, + s=pre.signature_s, + ), + ) + + if eip2718_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(encoded_signed_tx[1:]) + + return SignedTransaction( + 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, + r=pre.signatureR, + s=pre.signatureS, + ), + ) + + if 0xc0 <= eip2718_type <= 0xfe: + 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(encoded_signed_tx) + + if pre.v not in (27, 28): + 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, + 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=y_parity, + r=pre.r, + s=pre.s, + ), + ) + + assert False +``` + +## 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 `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`. + +- 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`. + +- 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. + +- 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. + +### Why `tx_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`. + +## Backwards Compatibility + +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. + +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. + +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 + +TBD + +## Reference Implementation + +TBD + +## Security Considerations + +None + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md).