Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-6404: SSZ 交易

将 RLP 交易迁移到 SSZ

Authors Etan Kissling (@etan-status), Gajinder Singh (@g11tech), Vitalik Buterin (@vbuterin)
Created 2023-01-30
Discussion Link https://ethereum-magicians.org/t/eip-6404-ssz-transactions/12783
Requires EIP-155, EIP-1559, EIP-2718, EIP-2930, EIP-4844, EIP-5793, EIP-7495, EIP-7702

摘要

本 EIP 定义了 EIP-2718 递归长度前缀(RLP)交易到 简单序列化(SSZ) 的迁移过程。

动机

RLP 交易存在许多缺点:

  1. 线性哈希: RLP 交易的签名哈希(sig_hash)和唯一标识符(tx_hash)是通过对其序列化进行线性 keccak256 哈希计算得出的。即使只对部分数据感兴趣,线性哈希也需要完整的交易数据存在,包括可能很大的 calldata 或访问列表。这也适用于基于 sig_hash 计算交易的 from 地址时。

  2. 低效的包含证明: 支持执行区块头的 transactions_root 的 Merkle-Patricia Trie (MPT) 是由序列化的交易所构建的,在内部将前缀添加到交易数据中,然后再将其 keccak256 哈希到 MPT 中。由于此前缀,链上没有对 tx_hash 的承诺,并且包含证明需要完整的交易数据存在。

  3. 不兼容的表示: 作为共识 ExecutionPayload 的一部分,交易的 RLP 序列化使用 SSZ merkleization 进行哈希处理。这些 SSZ 哈希与 tx_hash 和 MPT transactions_root 都不兼容。

  4. 不可扩展性: 交易类型无法通过可选功能进行扩展。假设从一开始就存在 EIP-4844 blob 交易,那么 EIP-2930 访问列表和 EIP-1559 优先级费用等新功能将需要两种新的交易类型,以同时扩展基本交易类型和 blob 交易类型。

  5. 技术债务: 所有处理 RLP 交易的客户端应用程序和智能合约都必须正确处理诸如缺少前缀字节的 LegacyTransaction、不一致的 chain_idv / y_parity 语义以及在其他字段之间引入 max_priority_fee_per_gas 等注意事项,而不是在末尾引入。由于现有的交易类型往往会永久保持有效,因此这种技术债务会随着时间的推移而累积。

  6. 不适当的不透明性: 共识层将 RLP 交易数据视为不透明的,但需要根据交易 blob_versioned_hashes 验证共识 blob_kzg_commitments,从而导致比必要的引擎 API 更复杂。

本 EIP 定义了一种无损转换机制,用于规范跨共识层和执行层的交易表示,同时保留对处理 RLP 交易类型的支持。

规范

本文档中的关键词“必须(MUST)”、“禁止(MUST NOT)”、“必需(REQUIRED)”、“应(SHALL)”、“不应(SHALL NOT)”、“应该(SHOULD)”、“不应该(SHOULD NOT)”、“推荐(RECOMMENDED)”、“不推荐(NOT RECOMMENDED)”、“可以(MAY)”和“可选(OPTIONAL)”按照 RFC 2119 和 RFC 8174 中的描述进行解释。

现有定义

为了参考,此处复制了本文档中使用的现有规范中的定义。

名称
MAX_TRANSACTIONS_PER_PAYLOAD uint64(2**20) (= 1,048,576)
BYTES_PER_FIELD_ELEMENT uint64(32)
FIELD_ELEMENTS_PER_BLOB uint64(4096)
MAX_BLOB_COMMITMENTS_PER_BLOCK uint64(2**12) (= 4,096)
名称 SSZ 等效项
Hash32 Bytes32
ExecutionAddress Bytes20
VersionedHash Bytes32
KZGCommitment Bytes48
KZGProof Bytes48
Blob ByteVector[BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB]

ExecutionSignature 容器

签名使用其原生的、不透明的表示形式,并扩展为链上对签名地址的承诺。

名称 描述
SECP256K1_SIGNATURE_SIZE 32 + 32 + 1 (= 65) secp256k1 ECDSA 签名的字节长度
MAX_EXECUTION_SIGNATURE_FIELDS uint64(2**3) (= 8) ExecutionSignature 未来可能增长到的最大字段数
class ExecutionSignature(StableContainer[MAX_EXECUTION_SIGNATURE_FIELDS]):
    secp256k1: Optional[ByteVector[SECP256K1_SIGNATURE_SIZE]]

class Secp256k1ExecutionSignature(Profile[ExecutionSignature]):
    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:])

Transaction 容器

所有交易都表示为单个、标准化的 SSZ 容器。该定义使用 EIP-7495 中定义的 StableContainer[N] SSZ 类型和 Optional[T]

名称 描述
MAX_FEES_PER_GAS_FIELDS uint64(2**4) (= 16) FeesPerGas 未来可能增长到的最大字段数
MAX_CALLDATA_SIZE uint64(2**24) (= 16,777,216) 交易的最大 input calldata 字节长度
MAX_ACCESS_LIST_STORAGE_KEYS uint64(2**19) (= 524,288) 访问元组中存储键的最大数量
MAX_ACCESS_LIST_SIZE uint64(2**19) (= 524,288) access_list 中访问元组的最大数量
MAX_AUTHORIZATION_PAYLOAD_FIELDS uint64(2**4) (= 16) AuthorizationPayload 未来可能增长到的最大字段数
MAX_AUTHORIZATION_LIST_SIZE uint64(2**16) (= 65,536) authorization_list 中授权的最大数量
MAX_TRANSACTION_PAYLOAD_FIELDS uint64(2**5) (= 32) TransactionPayload 未来可能增长到的最大字段数
名称 SSZ 等效项 描述
TransactionType uint8 EIP-2718 交易类型,范围 [0x00, 0x7F]
ChainId uint64 EIP-155 链 ID
FeePerGas uint256 每单位 gas 的费用
GasAmount uint64 以 gas 为单位的数量
class FeesPerGas(StableContainer[MAX_FEES_PER_GAS_FIELDS]):
    regular: Optional[FeePerGas]

    # EIP-4844
    blob: Optional[FeePerGas]

class AccessTuple(Container):
    address: ExecutionAddress
    storage_keys: List[Hash32, MAX_ACCESS_LIST_STORAGE_KEYS]

class AuthorizationPayload(StableContainer[MAX_AUTHORIZATION_PAYLOAD_FIELDS]):
    magic: Optional[TransactionType]
    chain_id: Optional[ChainId]
    address: Optional[ExecutionAddress]
    nonce: Optional[uint64]

class Authorization(Container):
    payload: AuthorizationPayload
    signature: ExecutionSignature

class TransactionPayload(StableContainer[MAX_TRANSACTION_PAYLOAD_FIELDS]):
    # EIP-2718
    type_: Optional[TransactionType]

    # EIP-155
    chain_id: Optional[ChainId]

    nonce: Optional[uint64]
    max_fees_per_gas: Optional[FeesPerGas]
    gas: Optional[GasAmount]
    to: Optional[ExecutionAddress]
    value: Optional[uint256]
    input_: Optional[ByteList[MAX_CALLDATA_SIZE]]

    # EIP-2930
    access_list: Optional[List[AccessTuple, MAX_ACCESS_LIST_SIZE]]

    # EIP-1559
    max_priority_fees_per_gas: Optional[FeesPerGas]

    # EIP-4844
    blob_versioned_hashes: Optional[List[VersionedHash, MAX_BLOB_COMMITMENTS_PER_BLOCK]]

    # EIP-7702
    authorization_list: Optional[List[Authorization, MAX_AUTHORIZATION_LIST_SIZE]]

class Transaction(Container):
    payload: TransactionPayload
    signature: ExecutionSignature

Transaction 配置文件

EIP-7495 Profile 定义为有效交易提供了类型安全。它们原始的 RLP TransactionType 被保留,以便在必要时可以恢复它们原始的 RLP 表示形式以及关联的 sig_hashtx_hash 值。

class BasicFeesPerGas(Profile[FeesPerGas]):
    regular: FeePerGas

class BlobFeesPerGas(Profile[FeesPerGas]):
    regular: FeePerGas
    blob: FeePerGas

class RlpLegacyTransactionPayload(Profile[TransactionPayload]):
    type_: TransactionType
    chain_id: Optional[ChainId]
    nonce: uint64
    max_fees_per_gas: BasicFeesPerGas
    gas: GasAmount
    to: Optional[ExecutionAddress]
    value: uint256
    input_: ByteList[MAX_CALLDATA_SIZE]

class RlpLegacyTransaction(Container):
    payload: RlpLegacyTransactionPayload
    signature: Secp256k1ExecutionSignature

class RlpAccessListTransactionPayload(Profile[TransactionPayload]):
    type_: TransactionType
    chain_id: ChainId
    nonce: uint64
    max_fees_per_gas: BasicFeesPerGas
    gas: GasAmount
    to: Optional[ExecutionAddress]
    value: uint256
    input_: ByteList[MAX_CALLDATA_SIZE]
    access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE]

class RlpAccessListTransaction(Container):
    payload: RlpAccessListTransactionPayload
    signature: Secp256k1ExecutionSignature

class RlpFeeMarketTransactionPayload(Profile[TransactionPayload]):
    type_: TransactionType
    chain_id: ChainId
    nonce: uint64
    max_fees_per_gas: BasicFeesPerGas
    gas: GasAmount
    to: Optional[ExecutionAddress]
    value: uint256
    input_: ByteList[MAX_CALLDATA_SIZE]
    access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE]
    max_priority_fees_per_gas: BasicFeesPerGas

class RlpFeeMarketTransaction(Container):
    payload: RlpFeeMarketTransactionPayload
    signature: Secp256k1ExecutionSignature

class RlpBlobTransactionPayload(Profile[TransactionPayload]):
    type_: TransactionType
    chain_id: ChainId
    nonce: uint64
    max_fees_per_gas: BlobFeesPerGas
    gas: GasAmount
    to: ExecutionAddress
    value: uint256
    input_: ByteList[MAX_CALLDATA_SIZE]
    access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE]
    max_priority_fees_per_gas: BlobFeesPerGas
    blob_versioned_hashes: List[VersionedHash, MAX_BLOB_COMMITMENTS_PER_BLOCK]

class RlpBlobTransaction(Container):
    payload: RlpBlobTransactionPayload
    signature: Secp256k1ExecutionSignature

class RlpSetCodeAuthorizationPayload(Profile[AuthorizationPayload]):
    magic: TransactionType
    chain_id: Optional[ChainId]
    address: ExecutionAddress
    nonce: uint64

class RlpSetCodeAuthorization(Container):
    payload: RlpSetCodeAuthorizationPayload
    signature: Secp256k1ExecutionSignature

class RlpSetCodeTransactionPayload(Profile[TransactionPayload]):
    type_: TransactionType
    chain_id: ChainId
    nonce: uint64
    max_fees_per_gas: BasicFeesPerGas
    gas: GasAmount
    to: ExecutionAddress
    value: uint256
    input_: ByteList[MAX_CALLDATA_SIZE]
    access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE]
    max_priority_fees_per_gas: BasicFeesPerGas
    authorization_list: List[RlpSetCodeAuthorization, MAX_AUTHORIZATION_LIST_SIZE]

class RlpSetCodeTransaction(Container):
    payload: RlpSetCodeTransactionPayload
    signature: Secp256k1ExecutionSignature

提供了帮助程序来识别标准化 TransactionEIP-7495 Profile。类型系统确保存在 Profile 的所有必需字段,并且不存在排除的字段。

LEGACY_TX_TYPE = TransactionType(0x00)
ACCESS_LIST_TX_TYPE = TransactionType(0x01)
FEE_MARKET_TX_TYPE = TransactionType(0x02)
BLOB_TX_TYPE = TransactionType(0x03)
SET_CODE_TX_TYPE = TransactionType(0x04)
SET_CODE_TX_MAGIC = TransactionType(0x05)

def identify_authorization_profile(auth: Authorization) -> Type[Profile]:
    if auth.payload.magic == SET_CODE_TX_MAGIC:
        if auth.payload.chain_id == 0:
            raise Exception(f'Unsupported chain ID in Set Code RLP authorization: {auth}')
        return RlpSetCodeAuthorization

    raise Exception(f'Unsupported authorization: {auth}')

def identify_transaction_profile(tx: Transaction) -> Type[Profile]:
    if tx.payload.type_ == SET_CODE_TX_TYPE:
        for auth in tx.payload.authorization_list or []:
            auth = identify_authorization_profile(auth).from_base(auth)
            if not isinstance(auth, RlpSetCodeAuthorization):
                raise Exception(f'Unsupported authorization in Set Code RLP transaction: {tx}')
        return RlpSetCodeTransaction

    if tx.payload.type_ == BLOB_TX_TYPE:
        if (tx.payload.max_priority_fees_per_gas or FeesPerGas()).blob != 0:
            raise Exception(f'Unsupported blob priority fee in Blob RLP transaction: {tx}')
        return RlpBlobTransaction

    if tx.payload.type_ == FEE_MARKET_TX_TYPE:
        return RlpFeeMarketTransaction

    if tx.payload.type_ == ACCESS_LIST_TX_TYPE:
        return RlpAccessListTransaction

    if tx.payload.type_ == LEGACY_TX_TYPE:
        return RlpLegacyTransaction

    raise Exception(f'Unsupported transaction: {tx}')

要获取交易的 from 地址、其标识符或授权的 authority 地址,请参阅 EIP 资产 以获取 compute_sig_hashcompute_tx_hashcompute_auth_hash 的定义,这些定义考虑了各种交易类型。

执行区块头更改

执行区块头的 txs-root 从 MPT 转换为 SSZ。

transactions = List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD](
    tx_0, tx_1, tx_2, ...)

block_header.transactions_root = transactions.hash_tree_root()

引擎 API

在引擎 API 中,采用此 EIP 的 ExecutionPayload 版本中 transactions 字段的结构从 Array of DATA 更改为 Array of TransactionV1

TransactionV1 定义为映射到 SSZ Transaction 类型,如下所示:

  • payload: TransactionPayloadV1 - 包含 TransactionPayloadV1 结构字段的 OBJECT
  • signature: ExecutionSignatureV1 - 包含 ExecutionSignatureV1 结构字段的 OBJECT

TransactionPayloadV1 定义为映射到 SSZ TransactionPayload StableContainer,如下所示:

  • type: QUANTITY|null, 8 位或 null
  • chainId: QUANTITY|null, 256 位或 null
  • nonce: QUANTITY|null, 64 位或 null
  • maxFeesPerGas: FeesPerGasV1|null - 包含 FeesPerGasV1 结构字段的 OBJECTnull
  • gas: QUANTITY|null, 64 位或 null
  • to: DATA|null, 20 字节或 null
  • value: QUANTITY|null, 256 位或 null
  • input: DATA|null,0 到 MAX_CALLDATA_SIZE 字节或 null
  • accessList: Array of AccessTupleV1 - 0 到 MAX_ACCESS_LIST_SIZE OBJECT 条目,每个条目包含 AccessTupleV1 结构的字段,或 null
  • maxPriorityFeesPerGas: FeesPerGasV1|null - 包含 FeesPerGasV1 结构字段的 OBJECTnull
  • blobVersionedHashes: Array of DATA|null - 0 到 MAX_BLOB_COMMITMENTS_PER_BLOCK DATA 条目,每个条目包含 32 字节,或 null
  • authorizationList: Array of AuthorizationV1 - 0 到 MAX_AUTHORIZATION_LIST_SIZE OBJECT 条目,每个条目包含 AuthorizationV1 结构的字段,或 null

FeesPerGasV1 定义为映射到 SSZ FeesPerGas StableContainer,如下所示:

  • regular: QUANTITY|null, 256 位或 null
  • blob: QUANTITY|null, 256 位或 null

AccessTupleV1 定义为映射到 SSZ AccessTuple Container,如下所示:

  • address: DATA, 20 字节
  • storageKeys: Array of DATA - 0 到 MAX_ACCESS_LIST_STORAGE_KEYS DATA 条目,每个条目包含 32 字节

AuthorizationV1 定义为映射到 SSZ Authorization Container,如下所示:

  • payload: AuthorizationPayloadV1 - 包含 AuthorizationPayloadV1 结构字段的 OBJECT
  • signature: ExecutionSignatureV1 - 包含 ExecutionSignatureV1 结构字段的 OBJECT

AuthorizationPayloadV1 定义为映射到 SSZ AuthorizationPayload StableContainer,如下所示:

  • magic: QUANTITY|null, 8 位或 null
  • chainId: QUANTITY|null, 256 位或 null
  • address: DATA|null, 20 字节或 null
  • nonce: QUANTITY|null, 64 位或 null

ExecutionSignatureV1 定义为映射到 SSZ ExecutionSignature StableContainer,如下所示:

  • secp256k1: DATA|null, 65 字节或 null

共识 ExecutionPayload 更改

构建共识 ExecutionPayload 时,transactions 列表不再是不透明的,而是使用新的 Transaction 类型。

class ExecutionPayload(Container):
    ...
    transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]
    ...

SSZ PooledTransaction 容器

在交易 gossip 响应(PooledTransactions)期间,每个 Transaction 都被包装到 PooledTransaction 中。

名称 描述
MAX_POOLED_TRANSACTION_FIELDS uint64(2**3) (= 8) PooledTransaction 未来可能增长到的最大字段数
class BlobData(Container):
    blobs: List[Blob, MAX_BLOB_COMMITMENTS_PER_BLOCK]
    commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]
    proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK]

class PooledTransaction(StableContainer[MAX_POOLED_TRANSACTION_FIELDS]):
    tx: Optional[Transaction]
    blob_data: Optional[BlobData]

EIP-4844 中定义的其他验证约束也适用于定义 tx.payload.blob_versioned_hashesblob_data 的交易。

交易 gossip 公告

交易 gossip 公告(NewPooledTransactionHashes)中 types 元素 的语义已更改为匹配 ssz(PooledTransaction.active_fields())。与基本交易相比,获取 blob 交易的单独控制流被保留。

请注意,此更改将具有 blob_dataPooledTransactionactive_fields 映射到 0x03,这与 blob RLP 交易之前的 BLOB_TX_TYPE 前缀一致。

网络

通过 Ethereum Wire Protocol 交换 SSZ 交易时,使用以下 EIP-2718 兼容信封:

名称 描述
SSZ_TX_TYPE TransactionType(0x1f) 端点特定的 SSZ 对象
  • Transaction: SSZ_TX_TYPE || snappyFramed(ssz(Transaction))
  • PooledTransaction: SSZ_TX_TYPE || snappyFramed(ssz(PooledTransaction))

对象使用 SSZ 进行编码,并使用 Snappy 框架格式进行压缩,从而匹配共识对象的编码,如共识网络规范 中定义的那样。作为编码的一部分,会发出未压缩的对象长度;每个对象的建议强制执行的限制是 MAX_CHUNK_SIZE 字节。

实现应该继续支持接受 RLP 交易到其交易池中。但是,此类交易必须转换为 SSZ 才能包含到 ExecutionPayload 中。请参阅 EIP 资产 以获取从 RLP 转换为 SSZ 的参考实现,以及相应的测试用例。原始的 sig_hashtx_hash 在整个转换过程中都将保留。

理由

在执行区块内切换到单个、统一且向前兼容的交易格式,可以降低客户端应用程序和智能合约的实现复杂性。使用 SSZ 可以更容易地实现未来的用例,例如交易包含证明或将可验证的 calldata 的各个区块提交给智能合约。

还解决了各种协议效率低下的问题。虽然在 RLP 系统下交易数据被哈希多次,包括 (1) sig_hash,(2) tx_hash,(3) MPT 内部哈希和 (4) SSZ 内部哈希,但标准化的表示形式减少了哈希计数。此外,如果共识 blob_kzg_commitments 未根据交易 blob_versioned_hashes 进行验证,则共识层实现可以提前删除无效区块,并且不再需要查询执行层以进行区块哈希验证。

向后兼容性

依赖于区块头中已替换的 MPT transactions_root 的应用程序需要迁移到 SSZ transactions_root

虽然链上没有对 tx_hash 的承诺,但它已广泛用于 JSON-RPC 和 Ethereum Wire Protocol 中,以唯一标识交易。tx_hash 在从 RLP 转换为 SSZ 的过程中保持稳定。

从 RLP 交易到 SSZ 的转换是无损的。原始的 RLP sig_hashtx_hash 可以从 SSZ 表示形式中恢复。

安全考虑

版权

通过 CC0 放弃版权及相关权利。

Citation

Please cite this document as:

Etan Kissling (@etan-status), Gajinder Singh (@g11tech), Vitalik Buterin (@vbuterin), "EIP-6404: SSZ 交易 [DRAFT]," Ethereum Improvement Proposals, no. 6404, January 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6404.