10 KiB
eip | title | description | author | discussions-to | status | type | category | created | requires |
---|---|---|---|---|---|---|---|---|---|
6493 | SSZ Transaction Signature Scheme | Signature scheme for SSZ transactions | Etan Kissling (@etan-status), Matt Garnett (@lightclient), Vitalik Buterin (@vbuterin) | https://ethereum-magicians.org/t/eip-6493-ssz-transaction-signature-scheme/13050 | Draft | Standards Track | Core | 2023-02-24 | 155, 2124, 2718 |
Abstract
This EIP defines a signature scheme for Simple Serialize (SSZ) encoded transactions.
Motivation
Existing EIP-2718 transaction types first encoded in the RLP format, and then hashed using keccak256 for signing and finally (post signing) to generate a unique transaction identifier as well.
However for new transaction types that are encoded in the SSZ format (for e.g. EIP-4844 blob transactions), it is idiomatic to base their signature hash and their unique identifier on hash_tree_root
instead.
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 transaction schema
For each SSZ transaction type, its specification defines an SSZ type that contains all transaction data to be signed.
class XyzTransaction(View):
...
The specification also defines an SSZ type that represents the transaction's signature.
class XyzSignature(View):
...
The signed transaction combines the unsigned transaction data with its signature.
class XyzSignedTransaction(Container):
message: XyzTransaction
signature: XyzSignature
The unique transaction identifier is defined as this signed container's SSZ hash_tree_root
.
When representing such a transaction as part of the execution block header's txs-root
Merkle-Patricia Trie, this unique transaction identifier is embedded instead of the transaction's raw network representation.
Network configuration
Each SSZ transaction type is introduced to a network during a fork transition. For the new fork, the network-specific EIP-2124 FORK_HASH
is recorded. Furthermore, an EIP-2718 transaction type is assigned.
Domain types
A DomainType
range is defined for signing SSZ transactions.
Name | SSZ equivalent |
---|---|
DomainType |
Bytes4 |
TransactionType |
uint8 |
Name | Value |
---|---|
DOMAIN_EXECUTION_MASK |
DomainType('0x00000002') |
DOMAIN_EXECUTION_TRANSACTION_BASE |
DomainType('0x01000002') |
For a given EIP-2718 transaction type, the corresponding DomainType
can be derived.
def domain_type_for_transaction_type(tx_type: TransactionType) -> DomainType:
return DomainType(
DOMAIN_EXECUTION_TRANSACTION_BASE[0],
tx_type,
DOMAIN_EXECUTION_TRANSACTION_BASE[2],
DOMAIN_EXECUTION_TRANSACTION_BASE[3],
)
Signature scheme
When an SSZ transaction is signed, additional information is mixed into the signed hash to uniquely identify the underlying transaction type scheme as well as the operating network.
Name | SSZ equivalent | Description |
---|---|---|
tx_type |
TransactionType |
Assigned EIP-2718 transaction type |
tx_type_fork_version |
Version |
Historic EIP-2124 FORK_HASH of type's introduction |
genesis_hash |
Hash32 |
Blockhash of the first block on the chain |
chain_id |
uint256 |
EIP-155 chain ID at time of signature |
The following helper functions compute the Domain
for signing an SSZ transaction.
class ExecutionForkData(Container):
fork_version: Version
genesis_hash: Hash32
chain_id: uint256
def compute_execution_fork_data_root(
fork_version: Version,
genesis_hash: Hash32,
chain_id: uint256,
) -> Root:
return ExecutionForkData(
fork_version=fork_version,
genesis_hash=genesis_hash,
chain_id=chain_id,
).hash_tree_root()
def compute_execution_domain(
domain_type: DomainType,
fork_version: Version,
genesis_hash: Hash32,
chain_id: uint256,
) -> Domain:
fork_data_root = compute_execution_fork_data_root(fork_version, genesis_hash, chain_id)
return Domain(domain_type + fork_data_root[:28])
def compute_transaction_domain(
tx_type: TransactionType,
tx_type_fork_version: Version,
genesis_hash: Hash32,
chain_id: uint256,
) -> Domain:
domain_type = domain_type_for_transaction_type(tx_type)
return compute_execution_domain(domain_type, tx_type_fork_version, genesis_hash, chain_id)
The Root
to sign is computed using compute_signing_root
based on the unsigned transaction's hash_tree_root
and the additional information about the transaction type.
Rationale
Why not keccak256?
SSZ and RLP objects encode differently. Namely, in an encoded SSZ transaction, it is not guaranteed that the chain_id
and the signature are at the same location as in the RLP transaction. This could be problematic if two different networks accidentally use the same EIP-2718 transaction type number to define an RLP encoded transaction type on one network, but an SSZ encoded transaction type on the other. A signed transaction on one network could suddenly become a valid transaction on the other network.
Why the specific domain type value?
DomainType
is used in consensus to isolate signing domains for validating BLS signatures. So far, execution uses secp256k1 ECDSA signatures instead, so it is not strictly necessary to isolate consensus and execution domains from each other. However, with 4 bytes, avoiding collisions across layers is trivially possible and might be useful in future use cases.
Consensus designates DOMAIN_APPLICATION_MASK
as DomainType('0x00000001')
for vendor specific use. Therefore, the next bit was used to refer to execution specific domains.
Backwards Compatibility
The new signature scheme is solely used for new transaction types.
Existing software that incorrectly assumes that all transaction identifiers are based on keccak256
may have to be updated.
Test Cases
# Network configuration
GENESIS_HASH = Hash32('0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f')
CHAIN_ID = uint256(424242)
# Example SSZ transaction
EXAMPLE_TX_TYPE = TransactionType(0xab)
EXAMPLE_TX_TYPE_FORK_VERSION = Version('0x12345678')
class ExampleTransaction(Container):
chain_id: uint256
nonce: uint64
max_fee_per_gas: uint256
gas: uint64
tx_to: ExecutionAddress
tx_value: uint256
class ExampleSignature(ByteVector[65]):
pass
class ExampleSignedTransaction(Container):
message: ExampleTransaction
signature: ExampleSignature
def compute_example_sig_hash(message: ExampleTransaction) -> Hash32:
domain = compute_transaction_domain(
EXAMPLE_TX_TYPE,
EXAMPLE_TX_TYPE_FORK_VERSION,
GENESIS_HASH,
CHAIN_ID,
)
return compute_signing_root(message, domain)
def compute_example_tx_hash(signed_tx: ExampleSignedTransaction) -> Hash32:
return signed_tx.hash_tree_root()
# Example transaction
message = ExampleTransaction(
chain_id=CHAIN_ID,
nonce=42,
max_fee_per_gas=69123456789,
gas=21000,
tx_to=ExecutionAddress(bytes.fromhex('d8da6bf26964af9d7eed9e03e53415d37aa96045')),
tx_value=3_141_592_653,
)
sig_hash = compute_example_sig_hash(message)
privkey = PrivateKey()
raw_sig = privkey.ecdsa_sign_recoverable(sig_hash, raw=True)
sig, y_parity = privkey.ecdsa_recoverable_serialize(raw_sig)
assert y_parity in (0, 1)
signed_tx = ExampleSignedTransaction(
message=message,
signature=ExampleSignature(sig + bytes([y_parity])),
)
tx_hash = compute_example_tx_hash(signed_tx)
Reference Implementation
TBD
Security Considerations
SSZ does not guarantee that the signature
field always ends up in the same location. If the signature is variable-length, or if the unsigned transaction data is constant-length, the signature will be located at the end. Otherwise, it will be located at offset 4. This means that SSZ transactions of different types may share the same representation, but are interpreted differently. See example.
Even though the leading EIP-2718 transaction type byte is not directly incorporated into message.hash_tree_root()
, it is hashed into sig_hash
, together with enough additional information to ensure that the signature really pertains to a specific transaction scheme from a specific specification on a specific chain. Therefore, even if an attacker modifies the leading byte to trigger a different interpretation, the public key recovered from that different interpretation will not refer to a used ExecutionAddress
. This assumption holds as long as there is only a single signer.
Copyright
Copyright and related rights waived via CC0.