Alert Source Discuss
⚠️ Review Standards Track: Core

EIP-6493: SSZ Transaction Signature Scheme

Signature scheme for SSZ transactions

Authors Etan Kissling (@etan-status), Matt Garnett (@lightclient), Vitalik Buterin (@vbuterin)
Created 2023-02-24
Requires EIP-155, EIP-191, EIP-1559, EIP-2718, EIP-2930, EIP-4844, EIP-5793, EIP-7495

Abstract

This EIP defines a signature scheme for Simple Serialize (SSZ) encoded transactions.

Motivation

For each transaction, two perpetual hashes are derived.

  1. sig_hash is the hash of the unsigned transaction that is being signed. It is crucial that no two valid transactions ever share the same sig_hash.

  2. tx_hash is a unique identifier to refer to a signed transaction. This hash is used to refer to a transaction within the mempool, and remains stable after a transaction is included into a block.

For existing EIP-2718 Recursive-Length Prefix (RLP) transactions, these hashes are based on a linear keccak256 hash across their serialization.

For Simple Serialize (SSZ) transaction types, an alternative signature scheme based on SHA256 Merkle trees is defined in this EIP.

Furthermore, this EIP defines a conversion mechanism to achieve a consistent representation across both RLP and SSZ transactions and receipts.

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.

EIP-2718 transaction types

Name SSZ equivalent Description
TransactionType uint8 EIP-2718 transaction type, range [0x00, 0x7F]

The values 0x00 and 0x04 are marked as reserved EIP-2718 transaction types.

  • 0x00 indicates an EIP-2718 LegacyTransaction.
  • 0x04 indicates an SSZ Transaction as defined in this EIP.
Name Value Description
(n/a) None Untyped LegacyTransaction (‘Homestead’ scheme)
TRANSACTION_TYPE_LEGACY TransactionType(0x00) Untyped LegacyTransaction (EIP-155 scheme)
TRANSACTION_TYPE_EIP2930 TransactionType(0x01) EIP-2930 transaction
TRANSACTION_TYPE_EIP1559 TransactionType(0x02) EIP-1559 transaction
TRANSACTION_TYPE_EIP4844 TransactionType(0x03) EIP-4844 transaction
TRANSACTION_TYPE_SSZ TransactionType(0x04) SSZ Transaction

Note that 0x19 is reserved to prevent collision with ERC-191 signed data.

Existing definitions

Definitions from existing specifications that are used throughout this document are replicated here for reference.

Name SSZ equivalent
Hash32 Bytes32
ExecutionAddress Bytes20
KZGCommitment Bytes48
KZGProof Bytes48
Blob ByteVector[BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB]
VersionedHash Bytes32
Name Value
BYTES_PER_LOGS_BLOOM uint64(2**8) (= 256)
BYTES_PER_FIELD_ELEMENT uint64(32)
FIELD_ELEMENTS_PER_BLOB uint64(4096)
MAX_BLOB_COMMITMENTS_PER_BLOCK uint64(2**12) (= 4,096)

SSZ Transaction container

All SSZ transactions are represented as a single, normalized SSZ container. The definition uses the StableContainer[N] SSZ type and Optional[T] as defined in EIP-7495.

Name Value Description
MAX_CALLDATA_SIZE uint64(2**24) (= 16,777,216) Maximum input calldata byte length for a transaction
MAX_ACCESS_LIST_STORAGE_KEYS uint64(2**19) (= 524,288) Maximum number of storage keys within an access tuple
MAX_ACCESS_LIST_SIZE uint64(2**19) (= 524,288) Maximum number of access tuples within an access_list
ECDSA_SIGNATURE_SIZE 32 + 32 + 1 (= 65) Byte length of an ECDSA (secp256k1) signature
MAX_FEES_PER_GAS_FIELDS uint64(2**4) (= 16) Maximum number of fields to which FeesPerGas can ever grow in the future
MAX_TRANSACTION_PAYLOAD_FIELDS uint64(2**5) (= 32) Maximum number of fields to which TransactionPayload can ever grow in the future
MAX_TRANSACTION_SIGNATURE_FIELDS uint64(2**4) (= 16) Maximum number of fields to which TransactionSignature can ever grow in the future
Name SSZ equivalent Description
ChainId uint64 EIP-155 chain ID at time of signature
FeePerGas uint256 Fee per unit of gas, cannot overflow across an entire block
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 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[uint64]
    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]]

class TransactionSignature(StableContainer[MAX_TRANSACTION_SIGNATURE_FIELDS]):
    from_: Optional[ExecutionAddress]
    ecdsa_signature: Optional[ByteVector[ECDSA_SIGNATURE_SIZE]]

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

Valid transaction types can be defined using EIP-7495 Profile.

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

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

class EcdsaTransactionSignature(Profile[TransactionSignature]):
    from_: Optional[ExecutionAddress]
    ecdsa_signature: Optional[ByteVector[ECDSA_SIGNATURE_SIZE]]

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

class ReplayableTransaction(Container):
    payload: ReplayableTransactionPayload
    signature: EcdsaTransactionSignature

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

class LegacyTransaction(Container):
    payload: LegacyTransactionPayload
    signature: EcdsaTransactionSignature

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

class Eip2930Transaction(Container):
    payload: Eip2930TransactionPayload
    signature: EcdsaTransactionSignature

class Eip1559TransactionPayload(Profile[TransactionPayload]):
    type_: TransactionType
    chain_id: ChainId
    nonce: uint64
    max_fees_per_gas: BasicFeesPerGas
    gas: uint64
    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 Eip1559Transaction(Container):
    payload: Eip1559TransactionPayload
    signature: EcdsaTransactionSignature

class Eip4844TransactionPayload(Profile[TransactionPayload]):
    type_: TransactionType
    chain_id: ChainId
    nonce: uint64
    max_fees_per_gas: BlobFeesPerGas
    gas: uint64
    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 Eip4844Transaction(Container):
    payload: Eip4844TransactionPayload
    signature: EcdsaTransactionSignature

class BasicTransactionPayload(Profile[TransactionPayload]):
    chain_id: ChainId
    nonce: uint64
    max_fees_per_gas: BasicFeesPerGas
    gas: uint64
    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 BasicTransaction(Container):
    payload: BasicTransactionPayload
    signature: EcdsaTransactionSignature

class BlobTransactionPayload(Profile[TransactionPayload]):
    chain_id: ChainId
    nonce: uint64
    max_fees_per_gas: BlobFeesPerGas
    gas: uint64
    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 BlobTransaction(Container):
    payload: BlobTransactionPayload
    signature: EcdsaTransactionSignature

def select_transaction_profile(cls, value: Transaction) -> Type[Profile]:
    if value.payload.type_ is None:
        if value.payload.blob_versioned_hashes is not None:
            return BlobTransaction
        return BasicTransaction

    if value.payload.type_ == TRANSACTION_TYPE_EIP4844:
        return Eip4844Transaction

    if value.payload.type_ == TRANSACTION_TYPE_EIP1559:
        return Eip1559Transaction

    if value.payload.type_ == TRANSACTION_TYPE_EIP2930:
        return Eip2930Transaction

    if value.payload.chain_id is not None:
        return LegacyTransaction

    return ReplayableTransaction

Future specifications MAY:

  • Append fields to TransactionPayload and TransactionSignature
  • Adjust Profile types and update select_transaction_profile logic

Such changes do not affect how existing transactions serialize or merkleize.

Transaction merkleization

Transaction signature scheme

When an SSZ transaction is signed, additional information is mixed into the sig_hash to uniquely identify the underlying specifications. If other networks define additional transaction types, they MUST use a different DomainType for signing SSZ data.

Name Value Description
DOMAIN_TRANSACTION_SSZ DomainType('0x04000080) DomainType for signing SSZ transactions compatible with this EIP

The hash to sign sig_hash and the unique transaction identifier tx_hash are computed using hash_tree_root.

class ExecutionSigningData(Container):
    object_root: Root
    domain_type: DomainType

def compute_ssz_sig_hash(payload: TransactionPayload) -> Hash32:
    return Hash32(ExecutionSigningData(
        object_root=payload.hash_tree_root(),
        domain=DOMAIN_TRANSACTION_SSZ,
    ).hash_tree_root())

def compute_ssz_tx_hash(tx: Transaction) -> Hash32:
    assert tx.payload.type_ == TRANSACTION_TYPE_SSZ
    return Hash32(tx.hash_tree_root())

Transaction validation

As part of Transaction validation, the from address MUST be checked for consistency with the ecdsa_signature.

def ecdsa_pack_signature(y_parity: bool,
                         r: uint256,
                         s: uint256) -> ByteVector[ECDSA_SIGNATURE_SIZE]:
    return r.to_bytes(32, 'big') + s.to_bytes(32, 'big') + bytes([0x01 if y_parity else 0x00])

def ecdsa_unpack_signature(signature: ByteVector[ECDSA_SIGNATURE_SIZE]) -> tuple[bool, uint256, uint256]:
    y_parity = signature[64] != 0
    r = uint256.from_bytes(signature[0:32], 'big')
    s = uint256.from_bytes(signature[32:64], 'big')
    return (y_parity, r, s)

def ecdsa_validate_signature(signature: ByteVector[ECDSA_SIGNATURE_SIZE]):
    SECP256K1N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
    assert len(signature) == 65
    assert signature[64] in (0, 1)
    _, r, s = ecdsa_unpack_signature(signature)
    assert 0 < r < SECP256K1N
    assert 0 < s < SECP256K1N

def ecdsa_recover_from_address(signature: ByteVector[ECDSA_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:])

def validate_transaction(tx):
    ecdsa_validate_signature(tx.signature.ecdsa_signature)
    assert tx.signature.from_ == ecdsa_recover_from_address(
        tx.signature.ecdsa_signature,
        compute_sig_hash(tx),
    )

See EIP assets for a definition of compute_sig_hash that takes the various transaction types into account.

SSZ PooledTransaction container

During transaction gossip responses (PooledTransactions), each Transaction is wrapped into a PooledTransaction. The definition uses the StableContainer[N] SSZ type and Optional[T] as defined in EIP-7495.

Name Value Description
MAX_POOLED_TRANSACTION_FIELDS uint64(2**3) (= 8) Maximum number of fields to which PooledTransaction can ever grow in the future
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: Transaction
    blob_data: Optional[BlobData]

The same additional validation constraints as defined in EIP-4844 also apply to transactions that define tx.payload.blob_versioned_hashes or blob_data.

Future specifications MAY:

  • Add fields to the end of PooledTransactionPayload
  • Convert existing fields to Optional

Such changes do not affect how existing pooled transactions serialize, merkleize, or validate.

SSZ Receipt container

All SSZ receipts are represented as a single, normalized SSZ container. The definition uses the StableContainer[N] SSZ type and Optional[T] as defined in EIP-7495.

Name Value Description
MAX_TOPICS_PER_LOG 4 LOG0 through LOG4 opcodes allow 0-4 topics per log
MAX_LOG_DATA_SIZE uint64(2**24) (= 16,777,216) Maximum data byte length for a log
MAX_LOGS_PER_RECEIPT uint64(2**21) (= 2,097,152) Maximum number of entries within logs
MAX_RECEIPT_FIELDS uint64(2**5) (= 32) Maximum number of fields to which Receipt can ever grow in the future
class Log(Container):
    address: ExecutionAddress
    topics: List[Bytes32, MAX_TOPICS_PER_LOG]
    data: ByteList[MAX_LOG_DATA_SIZE]

class Receipt(StableContainer[MAX_RECEIPT_FIELDS]):
    root: Optional[Hash32]
    gas_used: Optional[uint64]
    contract_address: Optional[ExecutionAddress]
    logs_bloom: Optional[ByteVector[BYTES_PER_LOGS_BLOOM]]
    logs: Optional[List[Log, MAX_LOGS_PER_RECEIPT]]

    # EIP-658
    status: Optional[boolean]

Valid receipt types can be defined using EIP-7495 Profile.

class HomesteadReceipt(Profile[Receipt]):
    root: Hash32
    gas_used: uint64
    contract_address: Optional[ExecutionAddress]
    logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM]
    logs: List[Log, MAX_LOGS_PER_RECEIPT]

class BasicReceipt(Profile[Receipt]):
    gas_used: uint64
    contract_address: Optional[ExecutionAddress]
    logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM]
    logs: List[Log, MAX_LOGS_PER_RECEIPT]
    status: boolean

def select_receipt_profile(value: Receipt) -> Type[Profile]:
    if value.status is not None:
        return BasicReceipt

    return HomesteadReceipt

Future specifications MAY:

  • Add fields to the end of Receipt
  • Adjust Profile types and update select_receipt_profile logic

Such changes do not affect how existing receipts serialize or merkleize.

Receipt merkleization

Networking

When exchanging SSZ transactions and receipts via the Ethereum Wire Protocol, the following EIP-2718 compatible envelopes are used:

  • Transaction: TRANSACTION_TYPE_SSZ || snappyFramed(ssz)
  • PooledTransaction: TRANSACTION_TYPE_SSZ || snappyFramed(ssz(PooledTransaction))
  • Receipt: TRANSACTION_TYPE_SSZ || snappyFramed(ssz(Receipt))

Objects are encoded using SSZ and compressed using the Snappy framing format, matching the encoding of consensus objects as defined in the consensus networking specification. As part of the encoding, the uncompressed object length is emitted; the RECOMMENDED limit to enforce per object is MAX_CHUNK_SIZE bytes.

Implementations SHOULD continue to support accepting RLP transactions into their transaction pool. However, such transactions MUST be converted to SSZ for inclusion into an ExecutionPayload. See EIP assets for a reference implementation to convert from RLP to SSZ, as well as corresponding test cases. The original sig_hash and tx_hash are retained throughout the conversion process.

Transaction gossip announcements

The semantics of the types element in transaction gossip announcements (NewPooledTransactionHashes) is changed to match ssz(PooledTransaction.active_fields()):

types Description
0x00 Untyped LegacyTransaction (‘Homestead’ scheme, or EIP-155 scheme)
0x01 EIP-2930 transaction, or basic SSZ PooledTransaction without any additional auxiliary payloads
0x02 EIP-1559 transaction
0x03 EIP-4844 transaction, or SSZ PooledTransaction with blob_data

Engine API

When exchanging via the engine API, the structure of the transactions field in ExecutionPayload versions adopting this EIP is changed from Array of DATA to Array of TransactionV1.

TransactionV1 is defined to map onto the SSZ Transaction StableContainer, as follows:

  • payload: TransactionPayloadV1 - An OBJECT containing the fields of a TransactionPayloadV1 structure
  • signature: TransactionSignatureV1 - An OBJECT containing the fields of a TransactionSignatureV1 structure

TransactionPayloadV1 is defined to map onto the SSZ TransactionPayload StableContainer, as follows:

  • type: QUANTITY|null, 8 Bits or null
  • chainId: QUANTITY|null, 64 Bits or null
  • nonce: QUANTITY|null, 64 Bits or null
  • maxFeesPerGas: FeesPerGasV1|null - An OBJECT containing the fields of a FeesPerGasV1 structure or null
  • gas: QUANTITY|null, 64 Bits or null
  • to: DATA|null, 20 Bytes or null
  • value: QUANTITY|null, 256 Bits or null
  • input: DATA|null, 0 through MAX_CALLDATA_SIZE bytes or null
  • accessList: Array of AccessTupleV1 - 0 through MAX_ACCESS_LIST_SIZE OBJECT entries each containing the fields of an AccessTupleV1 structure, or null
  • maxPriorityFeesPerGas: FeesPerGasV1|null - An OBJECT containing the fields of a FeesPerGasV1 structure or null
  • blobVersionedHashes: Array of DATA|null - 0 through MAX_BLOB_COMMITMENTS_PER_BLOCK DATA entries each containing 32 Bytes, or null

FeesPerGasV1 is defined to map onto the SSZ FeesPerGas StableContainer, as follows:

  • regular: QUANTITY|null, 256 Bits or null
  • blob: QUANTITY|null, 256 Bits or null

AccessTupleV1 is defined to map onto the SSZ AccessTuple Container, as follows:

  • address: DATA, 20 Bytes
  • storageKeys: Array of DATA - 0 through MAX_ACCESS_LIST_STORAGE_KEYS DATA entries each containing 32 Bytes

TransactionSignatureV1 is defined to map onto the SSZ TransactionSignature StableContainer, as follows:

  • from: DATA|null, 20 Bytes or null
  • ecdsaSignature: DATA|null, 65 Bytes or null

Rationale

Why SSZ transactions?

  1. Transaction inclusion proofs: Currently, there is no commitment to the transaction hash stored on chain. Therefore, proving inclusion of a certain transaction within a block requires sending the entire transaction body, and proving a list of all transaction hashes within a block requires sending all transaction bodies. With SSZ, a transaction can be “summarized” by it’s hash_tree_root, unlocking transaction root proofs without sending all transaction bodies, and compact transaction inclusion proofs by root.

  2. Better for light clients: With SSZ, individual fields of a transaction or receipt can be proven. This allows light clients to obtain only fields relevant to them. Furthermore, common fields fields always merkleize at the same generalized indices, allowing existing verification logic to continue working even when future updates introduce additional transaction or receipt fields.

  3. Better for smart contracts: Smart contracts that validate transactions or receipts benefit from the ability to prove individual chunks of a transaction. Gas fees may be lower, and it becomes possible to process transactions and receipts that do not fully fit into calldata.

  4. Smaller data size: SSZ objects are typically compressed using Snappy framed compression. Transaction input and access_list fields as well as receipt logs_bloom and logs fields often contain a lot of zero bytes and benefit from this compression. Snappy framed compression allows sending sequences of transactions and receipts without having to recompress, and is designed to be computationally inexpensive.

Why include the from address in transactions?

For transactions converted from RLP, the sig_hash is computed from its original RLP representation. To avoid requiring API clients to implement the original RLP encoding and keccak hashing, the from address is included as part of the Transaction.

Note that this also eliminates the need for secp256k1 public key recovery when serving JSON-RPC API requests, as the from address is already known.

Furthermore, this allows early rejecting transactions with sender accounts that do not have sufficient balance, as the from account balance can be checked without the computationally expensive ecrecover.

Why include the contract_address in receipts?

Computing the address of a newly created contract requires RLP encoding and keccak hashing. Adding a commitment on-chain avoids requiring API clients to implement those formats.

Even though the contract_address is statically determinable from the corresponding Transaction alone, including it in the Receipt allows the mechanism by which it is computed to change in the future.

Why the ExecutionSigningData?

If other SSZ objects are being signed in the future, e.g., messages, it must be ensured that their hashes do not collide with transaction sig_hash. Mixing in a constant that indicates that sig_hash pertains to an SSZ transaction prevents such hash collisions.

What about EIP-2718 transaction types?

All SSZ transactions (including future ones) share the single EIP-2718 transaction type TRANSACTION_TYPE_SSZ. Future features can introduce new optional fields as well as new allowed combination of optional fields, as determined by select_transaction_profile.

This also reduces combinatorial explosion; for example, the access_list property could be made optional for all SSZ transactions without having to double the number of defined transaction types.

Why redefine types for NewPooledTransactionHashes?

The types element as introduced in eth/68 via EIP-5793 allows the receiving node better control over the data it fetches from the peer and allows throttling the download of specific types.

Current implementations primarily use types to distinguish type 0x03 blob transactions from basic type 0x00, 0x01 and 0x02 transactions. However, all SSZ Transaction use type 0x04 (TRANSACTION_TYPE_SSZ), eliminating this optimization potential.

To restore the optimization potential, types is redefined to indicate instead what auxiliary payloads are present in the PooledTransaction: SSZ blob transactions will share type 0x03 with RLP blob transactions, while basic SSZ transactions will be assigned type 0x01, which is currently also used for a basic RLP transaction type. Therefore, implementations will not require changes to distinguish blob transactions from basic transactions.

Why change from cumulative_gas_used to gas_used in receipts?

EIP-658 replaced the intermediate post-state root from receipts with a boolean status code. Replacing cumulative_gas_used with gas_used likewise replaces the final stateful field with a stateless one, unlocking future optimization potential as transaction receipts operating on distinct state no longer depend on their order. Furthermore, API clients no longer need to fetch information from multiple receipts if they want to validate the gas_used of an individual transaction.

What about Log data in receipts?

Log 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 and transaction input data are provided as a ByteVector for now.

Backwards Compatibility

The new transaction signature scheme is solely used for SSZ transactions.

Existing RLP transactions can be converted to SSZ transactions. Their original sig_hash and tx_hash can be recovered from their SSZ representation.

Existing RLP receipts can be converted to SSZ receipts. The full sequence of accompanying transactions must be known to fill-in the new contract_address field. Note that because JSON-RPC exposes the contract_address, implementations are already required to know the transaction before queries for receipts can be served.

Security Considerations

SSZ signatures MUST NOT collide with existing RLP transaction and message hashes.

As RLP messages are hashed using keccak256, and all SSZ objects are hashed using SHA256. These two hashing algorithms are both considered cryptographically secure and are based on fundamentally different approaches, minimizing the risk of hash collision between those two hashing algorithms.

Furthermore, RLP messages are hashed linearly across their serialization, while SSZ objects are hashed using a recursive Merkle tree. Having a different mechanism further reduce the risk of hash collisions.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Etan Kissling (@etan-status), Matt Garnett (@lightclient), Vitalik Buterin (@vbuterin), "EIP-6493: SSZ Transaction Signature Scheme [DRAFT]," Ethereum Improvement Proposals, no. 6493, February 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6493.