Skip to content

Commit 963683f

Browse files
authored
Update EIP-6493: Add RLP to SSZ conversion implementation
Merged by EIP-Bot.
1 parent d89f7d2 commit 963683f

6 files changed

Lines changed: 886 additions & 12 deletions

File tree

EIPS/eip-6493.md

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ For each transaction, two perpetual hashes are derived.
2525

2626
For existing [EIP-2718](./eip-2718.md) Recursive-Length Prefix (RLP) transactions, these hashes are based on a linear keccak256 hash across their serialization.
2727

28-
For [Simple Serialize (SSZ)](https://github.com/ethereum/consensus-specs/blob/ef434e87165e9a4c82a99f54ffd4974ae113f732/ssz/simple-serialize.md) transaction types, an alternative signature scheme based on SHA256 hash tree is defined in this EIP.
28+
For [Simple Serialize (SSZ)](https://github.com/ethereum/consensus-specs/blob/ef434e87165e9a4c82a99f54ffd4974ae113f732/ssz/simple-serialize.md) transaction types, an alternative signature scheme based on SHA256 Merkle trees is defined in this EIP.
2929

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

3232
## Specification
3333

@@ -67,8 +67,6 @@ Definitions from existing specifications that are used throughout this document
6767

6868
| Name | Value |
6969
| - | - |
70-
| [`MAX_BYTES_PER_TRANSACTION`](https://github.com/ethereum/consensus-specs/blob/ef434e87165e9a4c82a99f54ffd4974ae113f732/specs/bellatrix/beacon-chain.md#execution) | `uint64(2**30)` (= 1,073,741,824) |
71-
| [`MAX_TRANSACTIONS_PER_PAYLOAD`](https://github.com/ethereum/consensus-specs/blob/ef434e87165e9a4c82a99f54ffd4974ae113f732/specs/bellatrix/beacon-chain.md#execution) | `uint64(2**20)` (= 1,048,576) |
7270
| [`BYTES_PER_LOGS_BLOOM`](https://github.com/ethereum/consensus-specs/blob/ef434e87165e9a4c82a99f54ffd4974ae113f732/specs/bellatrix/beacon-chain.md#execution) | `uint64(2**8)` (= 256) |
7371
| [`MAX_BLOB_COMMITMENTS_PER_BLOCK`](https://github.com/ethereum/consensus-specs/blob/ef434e87165e9a4c82a99f54ffd4974ae113f732/specs/deneb/beacon-chain.md#execution) | `uint64(2**12)` (= 4,096) |
7472

@@ -183,16 +181,60 @@ class SigningData(Container):
183181
domain: Domain
184182

185183
def compute_ssz_sig_hash(payload: TransactionPayload, chain_id: ChainId) -> Hash32:
186-
return Hash32(hash_tree_root(SigningData(
187-
object_root=hash_tree_root(payload),
184+
return Hash32(SigningData(
185+
object_root=payload.hash_tree_root(),
188186
domain=compute_ssz_transaction_domain(chain_id),
189-
)))
187+
).hash_tree_root())
190188

191189
def compute_ssz_tx_hash(tx: SignedTransaction) -> Hash32:
192190
assert tx.signature.type_ == TRANSACTION_TYPE_SSZ
193191
return Hash32(tx.hash_tree_root())
194192
```
195193

194+
### Transaction validation
195+
196+
As part of validating a `SignedTransaction`, the `from` address MUST be checked for consistency with the `ecdsa_signature`.
197+
198+
```python
199+
def ecdsa_pack_signature(y_parity: bool,
200+
r: uint256,
201+
s: uint256) -> ByteVector[ECDSA_SIGNATURE_SIZE]:
202+
return r.to_bytes(32, 'big') + s.to_bytes(32, 'big') + bytes([0x01 if y_parity else 0x00])
203+
204+
def ecdsa_unpack_signature(signature: ByteVector[ECDSA_SIGNATURE_SIZE]) -> tuple[bool, uint256, uint256]:
205+
y_parity = signature[64] != 0
206+
r = uint256.from_bytes(signature[0:32], 'big')
207+
s = uint256.from_bytes(signature[32:64], 'big')
208+
return (y_parity, r, s)
209+
210+
def ecdsa_validate_signature(signature: ByteVector[ECDSA_SIGNATURE_SIZE]):
211+
SECP256K1N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
212+
assert len(signature) == 65
213+
assert signature[64] in (0, 1)
214+
_, r, s = ecdsa_unpack_signature(signature)
215+
assert 0 < r < SECP256K1N
216+
assert 0 < s < SECP256K1N
217+
218+
def ecdsa_recover_from_address(signature: ByteVector[ECDSA_SIGNATURE_SIZE],
219+
sig_hash: Hash32) -> ExecutionAddress:
220+
ecdsa = ECDSA()
221+
recover_sig = ecdsa.ecdsa_recoverable_deserialize(signature[0:64], signature[64])
222+
public_key = PublicKey(ecdsa.ecdsa_recover(sig_hash, recover_sig, raw=True))
223+
uncompressed = public_key.serialize(compressed=False)
224+
return ExecutionAddress(keccak(uncompressed[1:])[12:])
225+
226+
def validate_transaction(tx: SignedTransaction,
227+
chain_id: ChainId):
228+
check_transaction_supported(tx)
229+
ecdsa_validate_signature(tx.signature.ecdsa_signature)
230+
assert tx.signature.from_ == ecdsa_recover_from_address(
231+
tx.signature.ecdsa_signature,
232+
compute_sig_hash(tx, chain_id),
233+
)
234+
```
235+
236+
See [EIP assets](../assets/eip-6493/tx_hashes.py) for a definition of `compute_sig_hash` that takes the various transaction types into account.
237+
196238
### SSZ `Receipt` container
197239

198240
All SSZ receipts are represented as a single, normalized SSZ container. The definition uses the `PartialContainer[T, N]` SSZ type and `Optional[E]` as defined in [EIP-7495](./eip-7495.md).
@@ -208,7 +250,7 @@ All SSZ receipts are represented as a single, normalized SSZ container. The defi
208250
class Log(Container):
209251
address: ExecutionAddress
210252
topics: List[Bytes32, MAX_TOPICS_PER_LOG]
211-
data: ByteVector[MAX_LOG_DATA_SIZE]
253+
data: ByteList[MAX_LOG_DATA_SIZE]
212254

213255
@dataclass
214256
class ReceiptPayload:
@@ -219,7 +261,7 @@ class ReceiptPayload:
219261
logs: List[Log, MAX_LOGS_PER_RECEIPT]
220262

221263
# EIP-658
222-
status: Optional[bool]
264+
status: Optional[boolean]
223265

224266
class Receipt(PartialContainer[ReceiptPayload, MAX_RECEIPT_FIELDS]):
225267
pass
@@ -278,11 +320,13 @@ This also reduces combinatorial explosion; for example, the `access_list` proper
278320

279321
## Backwards Compatibility
280322

281-
The new signature scheme is solely used for new transactions.
323+
The new signature scheme is solely used for SSZ transactions.
324+
325+
Existing RLP transactions can be converted to SSZ transactions. Their original `sig_hash` and `tx_hash` can be recovered from their SSZ representation.
282326

283-
Existing RLP transactions can be converted to SSZ transactions for a normalized representation. Their original representation can be fully recovered from their SSZ representation, allowing bidirectional lossless conversion.
327+
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.
284328

285-
Existing RLP receipts can be converted to SSZ receipts for a normalized representation. The full sequence of accompanying transactions must be known to fill-in the new `contract_address` field. Note that JSON-RPC already exposes the `contract_address`, so implementations are already required to know the transaction before queries for receipts can be served.
329+
See [EIP assets](../assets/eip-6493/convert.py) for a reference implementation to convert from RLP to SSZ, as well as corresponding [test cases](../assets/eip-6493/convert_tests.py).
286330

287331
## Test Cases
288332

assets/eip-6493/convert.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
from typing import List as PyList
2+
from rlp import decode
3+
from rlp_types import *
4+
from ssz_types import *
5+
6+
def upgrade_rlp_transaction_to_ssz(pre_bytes: bytes,
7+
chain_id: ChainId) -> SignedTransaction:
8+
type_ = pre_bytes[0]
9+
10+
if type_ == 0x03: # EIP-4844
11+
pre = decode(pre_bytes[1:], Eip4844SignedTransaction)
12+
assert pre.chain_id == chain_id
13+
14+
assert pre.signature_y_parity in (0, 1)
15+
ecdsa_signature = ecdsa_pack_signature(
16+
pre.signature_y_parity != 0,
17+
pre.signature_r,
18+
pre.signature_s,
19+
)
20+
from_ = ecdsa_recover_from_address(ecdsa_signature, compute_eip4844_sig_hash(pre))
21+
22+
return SignedTransaction(
23+
payload=PartialContainer[TransactionPayload, MAX_TRANSACTION_PAYLOAD_FIELDS](
24+
nonce=pre.nonce,
25+
max_fee_per_gas=pre.max_fee_per_gas,
26+
gas=pre.gas_limit,
27+
to=ExecutionAddress(pre.destination),
28+
value=pre.amount,
29+
input_=pre.data,
30+
access_list=[AccessTuple(
31+
address=access_tuple[0],
32+
storage_keys=access_tuple[1]
33+
) for access_tuple in pre.access_list],
34+
max_priority_fee_per_gas=pre.max_priority_fee_per_gas,
35+
max_fee_per_blob_gas=pre.max_fee_per_blob_gas,
36+
blob_versioned_hashes=pre.blob_versioned_hashes,
37+
),
38+
signature=PartialContainer[TransactionSignature, MAX_TRANSACTION_SIGNATURE_FIELDS](
39+
from_=from_,
40+
ecdsa_signature=ecdsa_signature,
41+
type_=TRANSACTION_TYPE_EIP4844,
42+
),
43+
)
44+
45+
if type_ == 0x02: # EIP-1559
46+
pre = decode(pre_bytes[1:], Eip1559SignedTransaction)
47+
assert pre.chain_id == chain_id
48+
49+
assert pre.signature_y_parity in (0, 1)
50+
ecdsa_signature = ecdsa_pack_signature(
51+
pre.signature_y_parity != 0,
52+
pre.signature_r,
53+
pre.signature_s,
54+
)
55+
from_ = ecdsa_recover_from_address(ecdsa_signature, compute_eip1559_sig_hash(pre))
56+
57+
return SignedTransaction(
58+
payload=PartialContainer[TransactionPayload, MAX_TRANSACTION_PAYLOAD_FIELDS](
59+
nonce=pre.nonce,
60+
max_fee_per_gas=pre.max_fee_per_gas,
61+
gas=pre.gas_limit,
62+
to=ExecutionAddress(pre.destination) if len(pre.destination) > 0 else None,
63+
value=pre.amount,
64+
input_=pre.data,
65+
access_list=[AccessTuple(
66+
address=access_tuple[0],
67+
storage_keys=access_tuple[1]
68+
) for access_tuple in pre.access_list],
69+
max_priority_fee_per_gas=pre.max_priority_fee_per_gas,
70+
),
71+
signature=PartialContainer[TransactionSignature, MAX_TRANSACTION_SIGNATURE_FIELDS](
72+
from_=from_,
73+
ecdsa_signature=ecdsa_signature,
74+
type_=TRANSACTION_TYPE_EIP1559,
75+
),
76+
)
77+
78+
if type_ == 0x01: # EIP-2930
79+
pre = decode(pre_bytes[1:], Eip2930SignedTransaction)
80+
assert pre.chainId == chain_id
81+
82+
assert pre.signatureYParity in (0, 1)
83+
ecdsa_signature = ecdsa_pack_signature(
84+
pre.signatureYParity != 0,
85+
pre.signatureR,
86+
pre.signatureS
87+
)
88+
from_ = ecdsa_recover_from_address(ecdsa_signature, compute_eip2930_sig_hash(pre))
89+
90+
return SignedTransaction(
91+
payload=PartialContainer[TransactionPayload, MAX_TRANSACTION_PAYLOAD_FIELDS](
92+
nonce=pre.nonce,
93+
max_fee_per_gas=pre.gasPrice,
94+
gas=pre.gasLimit,
95+
to=ExecutionAddress(pre.to) if len(pre.to) > 0 else None,
96+
value=pre.value,
97+
input_=pre.data,
98+
access_list=[AccessTuple(
99+
address=access_tuple[0],
100+
storage_keys=access_tuple[1]
101+
) for access_tuple in pre.accessList],
102+
),
103+
signature=PartialContainer[TransactionSignature, MAX_TRANSACTION_SIGNATURE_FIELDS](
104+
from_=from_,
105+
ecdsa_signature=ecdsa_signature,
106+
type_=TRANSACTION_TYPE_EIP2930,
107+
),
108+
)
109+
110+
if 0xc0 <= type_ <= 0xfe: # Legacy
111+
pre = decode(pre_bytes, LegacySignedTransaction)
112+
113+
if pre.v not in (27, 28): # EIP-155
114+
assert pre.v in (2 * chain_id + 35, 2 * chain_id + 36)
115+
ecdsa_signature = ecdsa_pack_signature(
116+
(pre.v & 0x1) == 0,
117+
pre.r,
118+
pre.s,
119+
)
120+
from_ = ecdsa_recover_from_address(ecdsa_signature, compute_legacy_sig_hash(pre))
121+
122+
return SignedTransaction(
123+
payload=PartialContainer[TransactionPayload, MAX_TRANSACTION_PAYLOAD_FIELDS](
124+
nonce=pre.nonce,
125+
max_fee_per_gas=pre.gasprice,
126+
gas=pre.startgas,
127+
to=ExecutionAddress(pre.to) if len(pre.to) > 0 else None,
128+
value=pre.value,
129+
input_=pre.data,
130+
),
131+
signature=PartialContainer[TransactionSignature, MAX_TRANSACTION_SIGNATURE_FIELDS](
132+
from_=from_,
133+
ecdsa_signature=ecdsa_signature,
134+
type_=TRANSACTION_TYPE_LEGACY if (pre.v not in (27, 28)) else None,
135+
),
136+
)
137+
138+
assert False
139+
140+
class ContractAddressData(Serializable):
141+
fields = (
142+
('from_', Binary(20, 20)),
143+
('nonce', big_endian_int),
144+
)
145+
146+
def compute_contract_address(from_: ExecutionAddress,
147+
nonce: uint64) -> ExecutionAddress:
148+
return ExecutionAddress(keccak(encode(ContractAddressData(
149+
from_=from_,
150+
nonce=nonce,
151+
)))[12:32])
152+
153+
def upgrade_rlp_receipt_to_ssz(pre_bytes: bytes,
154+
prev_cumulative_gas_used: uint64,
155+
transaction: SignedTransaction) -> Receipt:
156+
type_ = pre_bytes[0]
157+
158+
if type_ in (0x03, 0x02, 0x01): # EIP-4844, EIP-1559, EIP-2930
159+
pre = decode(pre_bytes[1:], RlpReceipt)
160+
elif 0xc0 <= type_ <= 0xfe: # Legacy
161+
pre = decode(pre_bytes, RlpReceipt)
162+
else:
163+
assert False
164+
165+
if len(pre.post_state_or_status) == 32:
166+
root = pre.post_state_or_status
167+
status = None
168+
else:
169+
root = None
170+
status = len(pre.post_state_or_status) > 0 and pre.post_state_or_status[0] != 0
171+
172+
return Receipt(
173+
root=root,
174+
gas_used=pre.cumulative_gas_used - prev_cumulative_gas_used,
175+
contract_address=compute_contract_address(
176+
transaction.signature.from_,
177+
transaction.payload.nonce,
178+
) if transaction.payload.to is None else None,
179+
logs_bloom=pre.logs_bloom,
180+
logs=[Log(
181+
address=log[0],
182+
topics=log[1],
183+
data=log[2],
184+
) for log in pre.logs],
185+
status=status,
186+
)
187+
188+
def upgrade_rlp_receipts_to_ssz(pre_bytes_list: PyList[bytes],
189+
chain_id: ChainId,
190+
transactions: PyList[SignedTransaction]) -> PyList[Receipt]:
191+
receipts = []
192+
cumulative_gas_used = 0
193+
for i, pre_bytes in enumerate(pre_bytes_list):
194+
receipt = upgrade_rlp_receipt_to_ssz(pre_bytes, cumulative_gas_used, transactions[i])
195+
cumulative_gas_used += receipt.gas_used
196+
receipts.append(receipt)
197+
return receipts

0 commit comments

Comments
 (0)