Skip to content

Commit

Permalink
Merge 1e9e198 into 9ef859f
Browse files Browse the repository at this point in the history
  • Loading branch information
etan-status committed Feb 25, 2023
2 parents 9ef859f + 1e9e198 commit 0f13409
Show file tree
Hide file tree
Showing 4 changed files with 418 additions and 12 deletions.
29 changes: 17 additions & 12 deletions EIPS/eip-4844.md
Expand Up @@ -8,7 +8,7 @@ status: Review
type: Standards Track
category: Core
created: 2022-02-25
requires: 1559, 2718, 2930, 4895
requires: 1559, 2718, 2930, 4895, 6493
---

## Abstract
Expand Down Expand Up @@ -161,24 +161,29 @@ The execution layer verifies the wrapper validity against the inner `Transaction
random evaluation at two points derived from the commitment and blob data)


The signature is verified and `tx.origin` is calculated as follows:
The hashes for signature verification and determining the public transaction identifier are computed according to the [EIP-6493](./eip-6493.md) transaction signature scheme:

```python
def unsigned_tx_hash(tx: SignedBlobTransaction) -> Bytes32:
# The pre-image is prefixed with the transaction-type to avoid hash collisions with other tx hashers and types
return keccak256(BLOB_TX_TYPE + ssz.serialize(tx.message))
def unsigned_tx_hash(tx: SignedBlobTransaction) -> Root:
domain = compute_transaction_domain(
BLOB_TX_TYPE,
BLOB_TX_TYPE_FORK_VERSION,
GENESIS_HASH,
CHAIN_ID,
)
return compute_signing_root(tx.message, domain)

def get_origin(tx: SignedBlobTransaction) -> Address:
sig = tx.signature
# v = int(y_parity) + 27, same as EIP-1559
return ecrecover(unsigned_tx_hash(tx), int(sig.y_parity)+27, sig.r, sig.s)
def signed_tx_hash(tx: SignedBlobTransaction) -> Root:
return signed_tx.hash_tree_root()
```

The hash of a signed blob transaction should be computed as:
`tx.origin` is calculated as follows:

```python
def signed_tx_hash(tx: SignedBlobTransaction) -> Bytes32:
return keccak256(BLOB_TX_TYPE + ssz.serialize(tx))
def get_origin(tx: SignedBlobTransaction) -> Address:
sig = tx.signature
# v = int(y_parity) + 27, same as EIP-1559
return ecrecover(unsigned_tx_hash(tx), int(sig.y_parity)+27, sig.r, sig.s)
```

### Header extension
Expand Down
226 changes: 226 additions & 0 deletions EIPS/eip-6493.md
@@ -0,0 +1,226 @@
---
eip: 6493
title: SSZ Transaction Signature Scheme
description: Signature scheme for SSZ transactions
author: Etan Kissling (@etan-status), Vitalik Buterin (@vbuterin)
discussions-to: <URL>
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 are encoded in the RLP format, and then hashed using keccak256 for signing. For new transaction types that are encoded in the SSZ format, it is ideomatic to base their signature hash and their public 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):
...
```

Each 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 the 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, that 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) 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 validator 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).
32 changes: 32 additions & 0 deletions assets/eip-6493/security/collision.py
@@ -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()

0 comments on commit 0f13409

Please sign in to comment.