--- eip: 6404 title: SSZ transactions description: Migration of RLP transactions to SSZ author: Etan Kissling (@etan-status), Gajinder Singh (@g11tech), Vitalik Buterin (@vbuterin) discussions-to: https://ethereum-magicians.org/t/eip-6404-ssz-transactions/12783 status: Draft type: Standards Track category: Core created: 2023-01-30 requires: 155, 1559, 2718, 2930, 4844, 7495, 7702, 7916, 7932, 8016 --- ## Abstract This EIP defines a migration process of [EIP-2718](./eip-2718.md) Recursive-Length Prefix (RLP) transactions to [Simple Serialize (SSZ)](https://github.com/ethereum/consensus-specs/blob/b5c3b619887c7850a8c1d3540b471092be73ad84/ssz/simple-serialize.md). ## Motivation RLP transactions have a number of shortcomings: 1. **Linear hashing:** The signing hash (`sig_hash`) and unique identifier (`tx_hash`) of an RLP transaction are computed by linear keccak256 hashes across its serialization. Even if only partial data is of interest, linear hashes require the full transaction data to be present, including potentially large calldata or access lists. This also applies when computing the `from` address of a transaction based on the `sig_hash`. 2. **Inefficient inclusion proofs:** The Merkle-Patricia Trie (MPT) backing the execution block header's `transactions_root` is constructed from the serialized transactions, internally prepending a prefix to the transaction data before it is keccak256 hashed into the MPT. Due to this prefix, there is no on-chain commitment to the `tx_hash` and inclusion proofs require the full transaction data to be present. 3. **Incompatible representation:** As part of the consensus `ExecutionPayload`, the RLP serialization of transactions is hashed using SSZ merkleization. These SSZ hashes are incompatible with both the `tx_hash` and the MPT `transactions_root`. 4. **Technical debt:** All client applications and smart contracts handling RLP transactions have to correctly deal with caveats such as `LegacyTransaction` lacking a prefix byte, the inconsistent `chain_id` and `v` / `y_parity` semantics, and the introduction of `max_priority_fee_per_gas` between other fields instead of at the end. As existing transaction types tend to remain valid perpetually, this technical debt builds up over time. 5. **Inappropriate opaqueness:** The Consensus Layer treats RLP transaction data as opaque, but requires validation of consensus `blob_kzg_commitments` against transaction `blob_versioned_hashes`, resulting in a more complex than necessary engine API. This EIP addresses these by defining a lossless conversion mechanism to normalize transaction representation across both Consensus Layer and Execution Layer while retaining support for processing RLP transaction types. ## 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. ### Existing definitions Definitions from existing specifications that are used throughout this document are replicated here for reference. | Name | Value | | - | - | | [`BYTES_PER_FIELD_ELEMENT`](https://github.com/ethereum/consensus-specs/blob/b5c3b619887c7850a8c1d3540b471092be73ad84/specs/deneb/polynomial-commitments.md#constants) | `uint64(32)` | | [`FIELD_ELEMENTS_PER_BLOB`](https://github.com/ethereum/consensus-specs/blob/b5c3b619887c7850a8c1d3540b471092be73ad84/specs/deneb/polynomial-commitments.md#blob) | `uint64(4096)` | | Name | SSZ equivalent | | - | - | | [`Hash32`](https://github.com/ethereum/consensus-specs/blob/b5c3b619887c7850a8c1d3540b471092be73ad84/specs/phase0/beacon-chain.md#custom-types) | `Bytes32` | | [`ExecutionAddress`](https://github.com/ethereum/consensus-specs/blob/b5c3b619887c7850a8c1d3540b471092be73ad84/specs/bellatrix/beacon-chain.md#custom-types) | `Bytes20` | | [`VersionedHash`](https://github.com/ethereum/consensus-specs/blob/b5c3b619887c7850a8c1d3540b471092be73ad84/specs/deneb/beacon-chain.md#custom-types) | `Bytes32` | | [`KZGCommitment`](https://github.com/ethereum/consensus-specs/blob/b5c3b619887c7850a8c1d3540b471092be73ad84/specs/deneb/polynomial-commitments.md#custom-types) | `Bytes48` | | [`KZGProof`](https://github.com/ethereum/consensus-specs/blob/b5c3b619887c7850a8c1d3540b471092be73ad84/specs/deneb/polynomial-commitments.md#custom-types) | `Bytes48` | | [`Blob`](https://github.com/ethereum/consensus-specs/blob/b5c3b619887c7850a8c1d3540b471092be73ad84/specs/deneb/polynomial-commitments.md#custom-types) | `ByteVector[BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB]` | ### Signatures Transaction signatures are represented by their native, opaque representation, prefixed by a value indicating their algorithm. | Name | SSZ equivalent | | - | - | | `ExecutionSignature` | `ProgressiveByteList` | | `ExecutionSignatureAlgorithm` | `uint8` | #### Secp256k1 | Name | Value | Description | | - | - | - | | `SECP256K1_ALGORITHM` | `uint8(0xFF)` | Defined from [EIP-7932](./eip-7932.md) | #### Verification The following functions are imported from [EIP-7932](./eip-7932.md): - `calculate_penalty` - `validate_signature` - `verify_signature` - `pubkey_to_address` ```python def get_signature_gas_cost( signature: ExecutionSignature, sig_hash: Hash32, expected_algorithm: Optional[ExecutionSignatureAlgorithm]=None ) -> uint: assert len(signature) > 0 if expected_algorithm is not None: assert signature[0] == expected_algorithm return calculate_penalty(signature[0], sig_hash) def validate_execution_signature( signature: ExecutionSignature, expected_algorithm: Optional[ExecutionSignatureAlgorithm]=None, ): assert len(signature) > 0 if expected_algorithm is not None: assert signature[0] == expected_algorithm validate_signature(signature) def recover_execution_signer(signature: ExecutionSignature, sig_hash: Hash32) -> ExecutionAddress: public_key = verify_signature(sig_hash, signature) return pubkey_to_address(public_key, signature[0]) ``` ### Gas fees The different kinds of gas fees are combined into a single structure. | Name | SSZ equivalent | Description | | - | - | - | | `FeePerGas` | `uint256` | Fee per unit of gas | ```python class BasicFeesPerGas(ProgressiveContainer(active_fields=[1])): regular: FeePerGas class BlobFeesPerGas(ProgressiveContainer(active_fields=[1, 1])): regular: FeePerGas blob: FeePerGas ``` ### Normalized transactions RLP transactions are converted to a normalized SSZ representation. Their original RLP `TransactionType` is retained to enable recovery of their original RLP representation and associated `sig_hash` and historical `tx_hash` values. | Name | SSZ equivalent | Description | | - | - | - | | `TransactionType` | `uint8` | [EIP-2718](./eip-2718.md) transaction type, range `[0x00, 0x7F]` | | `ChainId` | `uint256` | [EIP-155](./eip-155.md) chain ID | | `GasAmount` | `uint64` | Amount in units of gas | #### Replayable legacy transactions The original RLP representation of these transactions is replayable across networks with different chain ID. ```python class RlpLegacyReplayableBasicTransactionPayload( ProgressiveContainer(active_fields=[1, 0, 1, 1, 1, 1, 1, 1]) ): type_: TransactionType # 0x00 nonce: uint64 max_fees_per_gas: BasicFeesPerGas gas: GasAmount to: ExecutionAddress value: uint256 input_: ProgressiveByteList class RlpLegacyReplayableCreateTransactionPayload( ProgressiveContainer(active_fields=[1, 0, 1, 1, 1, 0, 1, 1]) ): type_: TransactionType # 0x00 nonce: uint64 max_fees_per_gas: BasicFeesPerGas gas: GasAmount value: uint256 input_: ProgressiveByteList ``` #### EIP-155 legacy transactions These transactions are locked to a single [EIP-155](./eip-155.md) chain ID. ```python class RlpLegacyBasicTransactionPayload( ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 1, 1, 1]) ): type_: TransactionType # 0x00 chain_id: ChainId nonce: uint64 max_fees_per_gas: BasicFeesPerGas gas: GasAmount to: ExecutionAddress value: uint256 input_: ProgressiveByteList class RlpLegacyCreateTransactionPayload( ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 0, 1, 1]) ): type_: TransactionType # 0x00 chain_id: ChainId nonce: uint64 max_fees_per_gas: BasicFeesPerGas gas: GasAmount value: uint256 input_: ProgressiveByteList ``` #### Legacy transactions ```python RlpLegacyTransactionPayload = ( RlpLegacyReplayableBasicTransactionPayload | RlpLegacyReplayableCreateTransactionPayload | RlpLegacyBasicTransactionPayload | RlpLegacyCreateTransactionPayload ) ``` #### EIP-2930 access list transactions These transactions support specifying an [EIP-2930](./eip-2930.md) access list. ```python class AccessTuple(Container): address: ExecutionAddress storage_keys: ProgressiveList[Hash32] class RlpAccessListBasicTransactionPayload( ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 1, 1, 1, 1]) ): type_: TransactionType # 0x01 chain_id: ChainId nonce: uint64 max_fees_per_gas: BasicFeesPerGas gas: GasAmount to: ExecutionAddress value: uint256 input_: ProgressiveByteList access_list: ProgressiveList[AccessTuple] class RlpAccessListCreateTransactionPayload( ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 0, 1, 1, 1]) ): type_: TransactionType # 0x01 chain_id: ChainId nonce: uint64 max_fees_per_gas: BasicFeesPerGas gas: GasAmount value: uint256 input_: ProgressiveByteList access_list: ProgressiveList[AccessTuple] RlpAccessListTransactionPayload = ( RlpAccessListBasicTransactionPayload | RlpAccessListCreateTransactionPayload ) ``` #### EIP-1559 fee market transactions These transactions support specifying [EIP-1559](./eip-1559.md) priority fees. ```python class RlpBasicTransactionPayload( ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) ): type_: TransactionType # 0x02 chain_id: ChainId nonce: uint64 max_fees_per_gas: BasicFeesPerGas gas: GasAmount to: ExecutionAddress value: uint256 input_: ProgressiveByteList access_list: ProgressiveList[AccessTuple] max_priority_fees_per_gas: BasicFeesPerGas class RlpCreateTransactionPayload( ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 0, 1, 1, 1, 1]) ): type_: TransactionType # 0x02 chain_id: ChainId nonce: uint64 max_fees_per_gas: BasicFeesPerGas gas: GasAmount value: uint256 input_: ProgressiveByteList access_list: ProgressiveList[AccessTuple] max_priority_fees_per_gas: BasicFeesPerGas RlpFeeMarketTransactionPayload = ( RlpBasicTransactionPayload | RlpCreateTransactionPayload ) ``` #### EIP-4844 blob transactions These transactions support specifying [EIP-4844](./eip-4844.md) blobs. ```python class RlpBlobTransactionPayload( ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) ): type_: TransactionType # 0x03 chain_id: ChainId nonce: uint64 max_fees_per_gas: BlobFeesPerGas gas: GasAmount to: ExecutionAddress value: uint256 input_: ProgressiveByteList access_list: ProgressiveList[AccessTuple] max_priority_fees_per_gas: BasicFeesPerGas blob_versioned_hashes: ProgressiveList[VersionedHash] ``` #### EIP-7702 set code transactions These transactions support specifying an [EIP-7702](./eip-7702.md) authorization list. ```python class RlpReplayableBasicAuthorizationPayload(ProgressiveContainer(active_fields=[1, 0, 1, 1])): magic: TransactionType # 0x05 address: ExecutionAddress nonce: uint64 class RlpBasicAuthorizationPayload(ProgressiveContainer(active_fields=[1, 1, 1, 1])): magic: TransactionType # 0x05 chain_id: ChainId address: ExecutionAddress nonce: uint64 class RlpSetCodeAuthorizationPayload(CompatibleUnion({ 0x01: RlpReplayableBasicAuthorizationPayload, 0x02: RlpBasicAuthorizationPayload, })): pass class RlpSetCodeAuthorization(Container): payload: RlpSetCodeAuthorizationPayload signature: ExecutionSignature class RlpSetCodeTransactionPayload( ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1]) ): type_: TransactionType # 0x04 chain_id: ChainId nonce: uint64 max_fees_per_gas: BasicFeesPerGas gas: GasAmount to: ExecutionAddress value: uint256 input_: ProgressiveByteList access_list: ProgressiveList[AccessTuple] max_priority_fees_per_gas: BasicFeesPerGas authorization_list: ProgressiveList[RlpSetCodeAuthorization] ``` #### Transaction helpers ```python class TransactionPayload(CompatibleUnion({ 0x01: RlpLegacyReplayableBasicTransactionPayload, 0x02: RlpLegacyReplayableCreateTransactionPayload, 0x03: RlpLegacyBasicTransactionPayload, 0x04: RlpLegacyCreateTransactionPayload, 0x05: RlpAccessListBasicTransactionPayload, 0x06: RlpAccessListCreateTransactionPayload, 0x07: RlpBasicTransactionPayload, 0x08: RlpCreateTransactionPayload, 0x09: RlpBlobTransactionPayload, 0x0a: RlpSetCodeTransactionPayload, })): pass class Transaction(Container): payload: TransactionPayload signature: ExecutionSignature class RlpTxType(IntEnum): LEGACY = 0x00 ACCESS_LIST = 0x01 FEE_MARKET = 0x02 BLOB = 0x03 SET_CODE = 0x04 SET_CODE_MAGIC = 0x05 def calculate_transaction_intrinsic_gas(tx: Transaction) -> uint: tx_data = tx.payload.data() gas_cost = TX_BASE_COST # FIXME: Should this be defined from another EIP? if hasattr(tx_data, "authorization_list"): for auth in tx_data.authorization_list: gas_cost += get_signature_gas_cost(auth.signature, expected_algorithm=expected_signature_algorithm) gas_cost += get_signature_gas_cost(tx.signature, expected_algorithm=expected_signature_algorithm) return gas_cost def validate_transaction(tx: Transaction): tx_data = tx.payload.data() expected_signature_algorithm = None if hasattr(tx_data, "type_"): expected_signature_algorithm = SECP256K1_ALGORITHM match tx_data.type_: case RlpTxType.LEGACY: assert isinstance(tx_data, RlpLegacyTransactionPayload) case RlpTxType.ACCESS_LIST: assert isinstance(tx_data, RlpAccessListTransactionPayload) case RlpTxType.FEE_MARKET: assert isinstance(tx_data, RlpFeeMarketTransactionPayload) case RlpTxType.BLOB: assert isinstance(tx_data, RlpBlobTransactionPayload) case RlpTxType.SET_CODE: assert isinstance(tx_data, RlpSetCodeTransactionPayload) case _: assert False if hasattr(tx_data, "authorization_list"): for auth in tx_data.authorization_list: auth_data = auth.payload.data() if hasattr(auth_data, "magic"): assert auth_data.magic == RlpTxType.SET_CODE_MAGIC if hasattr(auth_data, "chain_id"): assert auth_data.chain_id != 0 validate_execution_signature(auth.signature, compute_auth_hash(auth), expected_algorithm=expected_signature_algorithm) validate_execution_signature(tx.signature, compute_sig_hash(tx), expected_algorithm=expected_signature_algorithm) ``` ### Execution block header changes The [execution block header's `txs-root`](https://github.com/ethereum/devp2p/blob/bc76b9809a30e6dc5c8dcda996273f0f9bcf7108/caps/eth.md#block-encoding-and-validity) is transitioned from MPT to SSZ. ```python transactions = ProgressiveList[Transaction]( tx_0, tx_1, tx_2, ...) block_header.transactions_root = transactions.hash_tree_root() ``` ### Engine API In the engine API, the semantics of the `transactions` field in `ExecutionPayload` versions adopting this EIP are changed to emit transactions using SSZ serialization. - `transactions` - `Array` of `DATA` - Array of transaction objects, each object is a byte list (`DATA`) representing `ssz.serialize(tx)` ### Consensus `ExecutionPayload` changes When building a consensus `ExecutionPayload`, the [`transactions`](https://github.com/ethereum/consensus-specs/blob/b5c3b619887c7850a8c1d3540b471092be73ad84/specs/deneb/beacon-chain.md#executionpayload) list is no longer opaque and uses the new `Transaction` type, aligning the `transactions_root` across execution blocks and execution payloads. ```python class ExecutionPayload(...): ... transactions: ProgressiveList[Transaction] ... ``` ### Unique transaction identifier For each transaction, an additional identifier `tx_root` SHALL be assigned: ```python def compute_tx_root(tx: Transaction) -> Hash32: return Hash32(tx.hash_tree_root()) ``` Note that this `tx_root` differs from the existing `tx_hash`. Existing APIs based on `tx_hash` remain available. ## Rationale ### Forward compatibility The proposed transaction design is extensible with new fee types, new signature types, and entirely new transaction features (e.g., CREATE2), while retaining compatibility with the proposed transactions. ### Verifier improvements The `transactions_root` is effectively constructed from the list of `tx_root`, enabling transaction inclusion proofs. Further, partial data becomes provable, such as destination / amount without requiring the full calldata. This can reduce gas cost or zk proving cost when verifying L2 chain data in an L1 smart contract. ### Consensus client improvements Consensus Layer implementations may drop invalid blocks early if consensus `blob_kzg_commitments` do not validate against transaction `blob_versioned_hashes` and no longer need to query the Execution Layer for that validation. Future versions of the engine API could be simplified to drop the transfers of `blob_kzg_commitments` to the EL. ## Backwards Compatibility Applications that rely on the replaced MPT `transactions_root` in the block header require migration to the SSZ `transactions_root`. While there is no on-chain commitment of the `tx_hash`, it is widely used in JSON-RPC and the [Ethereum Wire Protocol](https://github.com/ethereum/devp2p/blob/bc76b9809a30e6dc5c8dcda996273f0f9bcf7108/caps/eth.md) to uniquely identify transactions. The conversion from RLP transactions to SSZ is lossless. The original RLP `sig_hash` and `tx_hash` can be recovered from the SSZ representation. RLP and SSZ transactions may clash when encoded. It is essential to use only a single format within one channel. ## Security Considerations None ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).