-
Notifications
You must be signed in to change notification settings - Fork 5.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add EIP-6493: SSZ Transaction Signature Scheme #6493
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
0910fa8
Update EIP-4844: `hash_tree_root` based transaction hashes
etan-status 4e8c2ac
Fix `signed_tx_hash`
etan-status b949308
Use static chain ID for purpose of domain computation
etan-status e279273
Merge branch 'master' into ps-htr
etan-status 1e9e198
Extract SSZ signature scheme to EIP for reuse
etan-status 619be29
Add URL
etan-status f77c446
Split 4844 changes to separate PR
etan-status d17e078
Fix
etan-status 0b5012c
rm redundant test
etan-status b322bc6
Apply most of @g11tech's review
Pandapip1 File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
--- | ||
eip: 6493 | ||
title: SSZ Transaction Signature Scheme | ||
description: Signature scheme for SSZ transactions | ||
author: Etan Kissling (@etan-status), Matt Garnett (@lightclient), Vitalik Buterin (@vbuterin) | ||
discussions-to: https://ethereum-magicians.org/t/eip-6493-ssz-transaction-signature-scheme/13050 | ||
status: Draft | ||
type: Standards Track | ||
category: Core | ||
created: 2023-02-24 | ||
requires: 155, 2124, 2718 | ||
--- | ||
|
||
## Abstract | ||
|
||
This EIP defines a signature scheme for [Simple Serialize (SSZ)](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/ssz/simple-serialize.md) encoded transactions. | ||
|
||
## Motivation | ||
|
||
Existing [EIP-2718](./eip-2718.md) 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](./eip-4844.md) 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. | ||
|
||
```python | ||
class XyzTransaction(View): | ||
... | ||
``` | ||
|
||
The specification also defines an SSZ type that represents the transaction's signature. | ||
|
||
```python | ||
class XyzSignature(View): | ||
... | ||
``` | ||
|
||
The signed transaction combines the unsigned transaction data with its signature. | ||
|
||
```python | ||
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`](https://github.com/ethereum/devp2p/blob/bd17dac4228c69b6379644355f373669f74952cd/caps/eth.md#block-encoding-and-validity) 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](./eip-2124.md#specification) `FORK_HASH` is recorded. Furthermore, an [EIP-2718](./eip-2718.md) transaction type is assigned. | ||
|
||
### Domain types | ||
|
||
A [`DomainType`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types) range is defined for signing SSZ transactions. | ||
|
||
| Name | SSZ equivalent | | ||
| - | - | | ||
| [`DomainType`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types) | `Bytes4` | | ||
| `TransactionType` | `uint8` | | ||
|
||
| Name | Value | | ||
| - | - | | ||
| `DOMAIN_EXECUTION_MASK` | `DomainType('0x00000002')` | ||
| `DOMAIN_EXECUTION_TRANSACTION_BASE` | `DomainType('0x01000002')` | | ||
|
||
For a given [EIP-2718](./eip-2718.md) transaction type, the corresponding [`DomainType`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types) can be derived. | ||
|
||
```python | ||
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](./eip-2718) transaction type | | ||
| `tx_type_fork_version` | [`Version`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types) | Historic [EIP-2124](./eip-2124.md#specification) `FORK_HASH` of type's introduction | | ||
| `genesis_hash` | [`Hash32`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types) | Blockhash of the first block on the chain | | ||
| `chain_id` | `uint256` | [EIP-155](./eip-155.md) chain ID at time of signature | | ||
|
||
The following helper functions compute the [`Domain`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types) for signing an SSZ transaction. | ||
|
||
```python | ||
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`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types) to sign is computed using [`compute_signing_root`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#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](./eip-2718.md) 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`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#custom-types) 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`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/phase0/beacon-chain.md#domain-types) 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 | ||
|
||
```python | ||
# 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](../assets/eip-6493/security/collision.py). | ||
|
||
Even though the leading [EIP-2718](./eip-2718.md) 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](../LICENSE.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
from remerkleable.byte_arrays import ByteList, ByteVector | ||
from remerkleable.complex import Container | ||
|
||
class Signature(ByteVector[65]): | ||
pass | ||
|
||
class FooTransaction(Container): | ||
x: ByteVector[73] | ||
|
||
class FooSignedTransaction(Container): | ||
message: FooTransaction | ||
signature: Signature | ||
|
||
class BarTransaction(Container): | ||
x: ByteList[65] | ||
|
||
class BarSignedTransaction(Container): | ||
message: BarTransaction | ||
signature: Signature | ||
|
||
FooSignedTransaction( | ||
message=FooTransaction( | ||
x=bytes.fromhex('45000000bf9f6e691dc8d90c41308b3155baf8095a3b10b4ddf8cac808cb755ba872e14b5ee6f49aa43f89715d24235bf49ffa6cd30179e44e360fc0458bc5b4f3458fc31404000000'), | ||
), | ||
signature=bytes.fromhex('a70279ba3b0c56b04cbaeb805e5f36e9214427f2df26d906e0e05cd013cb25463dec281a060b3df5d240c1354113fbdbac70d2f1392002dbd0b7a4f991a0996001'), | ||
).encode_bytes() == \ | ||
BarSignedTransaction( | ||
message=BarTransaction( | ||
x=bytes.fromhex('a70279ba3b0c56b04cbaeb805e5f36e9214427f2df26d906e0e05cd013cb25463dec281a060b3df5d240c1354113fbdbac70d2f1392002dbd0b7a4f991a0996001'), | ||
), | ||
signature=bytes.fromhex('f3d2ea8ea566a9ce680fc638a00127f183f22c98eceb0d909b7c8660faca3a46648a9da6f40f515cb39d18dbf5f4526a052736832a8bb6234b27e9e536a531a400'), | ||
).encode_bytes() |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would this be clearer to convey that this goes into signature calc