SSZ transactions
相关视频
正文
Abstract
This EIP defines a migration process of EIP-2718 Recursive-Length Prefix (RLP) transactions to Simple Serialize (SSZ).
Motivation
RLP transactions have a number of shortcomings:
-
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 thefrom
address of a transaction based on thesig_hash
. -
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 thetx_hash
and inclusion proofs require the full transaction data to be present. -
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 thetx_hash
and the MPTtransactions_root
. -
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 inconsistentchain_id
andv
/y_parity
semantics, and the introduction ofmax_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. -
Inappropriate opaqueness: The Consensus Layer treats RLP transaction data as opaque, but requires validation of consensus
blob_kzg_commitments
against transactionblob_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 | uint64(32) |
FIELD_ELEMENTS_PER_BLOB | uint64(4096) |
Name | SSZ equivalent |
---|---|
Hash32 | Bytes32 |
ExecutionAddress | Bytes20 |
VersionedHash | Bytes32 |
KZGCommitment | Bytes48 |
KZGProof | Bytes48 |
Blob | ByteVector[BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB] |
Signatures
Transaction signatures are represented by their native, opaque representation. ECDSA signatures are no longer split up into r
, s
, and y_parity
components.
Name | Value | Description |
---|---|---|
SECP256K1_SIGNATURE_SIZE | 32 + 32 + 1 (= 65) | Byte length of a secp256k1 ECDSA signature |
class Secp256k1ExecutionSignature(ProgressiveContainer[active_fields=[1]]): secp256k1: ByteVector[SECP256K1_SIGNATURE_SIZE] def secp256k1_pack(r: uint256, s: uint256, y_parity: uint8) -> ByteVector[SECP256K1_SIGNATURE_SIZE]: return r.to_bytes(32, 'big') + s.to_bytes(32, 'big') + bytes([y_parity]) def secp256k1_unpack(signature: ByteVector[SECP256K1_SIGNATURE_SIZE]) -> tuple[uint256, uint256, uint8]: r = uint256.from_bytes(signature[0:32], 'big') s = uint256.from_bytes(signature[32:64], 'big') y_parity = signature[64] return (r, s, y_parity) def secp256k1_validate(signature: ByteVector[SECP256K1_SIGNATURE_SIZE]): SECP256K1N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 r, s, y_parity = secp256k1_unpack(signature) assert 0 < r < SECP256K1N assert 0 < s <= SECP256K1N // 2 assert y_parity in (0, 1) def secp256k1_recover_signer(signature: ByteVector[SECP256K1_SIGNATURE_SIZE], sig_hash: Hash32) -> ExecutionAddress: ecdsa = ECDSA() recover_sig = ecdsa.ecdsa_recoverable_deserialize(signature[0:64], signature[64]) public_key = PublicKey(ecdsa.ecdsa_recover(sig_hash, recover_sig, raw=True)) uncompressed = public_key.serialize(compressed=False) return ExecutionAddress(keccak(uncompressed[1:])[12:])
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 |
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.
class Transaction(CompatibleUnion[ RlpTransaction, ]): pass class RlpTransaction(CompatibleUnion[ RlpLegacyReplayableBasicTransaction, RlpLegacyReplayableCreateTransaction, RlpLegacyBasicTransaction, RlpLegacyCreateTransaction, RlpAccessListBasicTransaction, RlpAccessListCreateTransaction, RlpBasicTransaction, RlpCreateTransaction, RlpBlobTransaction, RlpSetCodeTransaction, ]): pass
Name | SSZ equivalent | Description |
---|---|---|
TransactionType | uint8 | EIP-2718 transaction type, range [0x00, 0x7F] |
ChainId | uint256 | EIP-155 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.
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 RlpLegacyReplayableBasicTransaction(Container): payload: RlpLegacyReplayableBasicTransactionPayload signature: Secp256k1ExecutionSignature 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 class RlpLegacyReplayableCreateTransaction(Container): payload: RlpLegacyReplayableCreateTransactionPayload signature: Secp256k1ExecutionSignature
EIP-155 legacy transactions
These transactions are locked to a single EIP-155 chain ID.
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 RlpLegacyBasicTransaction(Container): payload: RlpLegacyBasicTransactionPayload signature: Secp256k1ExecutionSignature 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 class RlpLegacyCreateTransaction(Container): payload: RlpLegacyCreateTransactionPayload signature: Secp256k1ExecutionSignature
EIP-2930 access list transactions
These transactions support specifying an EIP-2930 access list.
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 RlpAccessListBasicTransaction(Container): payload: RlpAccessListBasicTransactionPayload signature: Secp256k1ExecutionSignature 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] class RlpAccessListCreateTransaction(Container): payload: RlpAccessListCreateTransactionPayload signature: Secp256k1ExecutionSignature
EIP-1559 fee market transactions
These transactions support specifying EIP-1559 priority fees.
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 RlpBasicTransaction(Container): payload: RlpBasicTransactionPayload signature: Secp256k1ExecutionSignature 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 class RlpCreateTransaction(Container): payload: RlpCreateTransactionPayload signature: Secp256k1ExecutionSignature
EIP-4844 blob transactions
These transactions support specifying EIP-4844 blobs.
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] class RlpBlobTransaction(Container): payload: RlpBlobTransactionPayload signature: Secp256k1ExecutionSignature
EIP-7702 set code transactions
These transactions support specifying an EIP-7702 authorization list.
class RlpAuthorization(CompatibleUnion[ RlpReplayableBasicAuthorizationPayload, RlpBasicAuthorizationPayload, ]): pass 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 RlpSetCodeTransactionPayload( ProgressiveContainer[active_fields=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 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] class RlpSetCodeTransaction(Container): payload: RlpAuthorization signature: Secp256k1ExecutionSignature
Execution block header changes
The execution block header's txs-root
is transitioned from MPT to SSZ.
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
ofDATA
- Array of transaction objects, each object is a byte list (DATA
) representingssz.serialize(tx)
Consensus ExecutionPayload
changes
When building a consensus ExecutionPayload
, the transactions
list is no longer opaque and uses the new Transaction
type, aligning the transactions_root
across execution blocks and execution payloads.
class ExecutionPayload(...): ... transactions: ProgressiveList[Transaction] ...
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
Future RPC could expose an SSZ based tx_root
on top of the tx_hash
, against which proofs can be validated. The transactions_root
can now be reconstructed from the list of tx_root
. 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 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.