-
-
Published
Linked with GitHub
# Proposed transaction SSZ refactoring for Cancun
## Goals
* Transition from RLP to SSZ
* Introduce the blob transaction type needed for EIP-4844
* Preserve backwards-compatiblity for nost users and applications, but be okay with limited exceptions (eg. apps that recompute the `transactionsRoot` from RPC responses, or that depend on in-EVM history proofs, would break)
* Do all the work at once to avoid extra costs from engineering a multi-step transition
## Core ideas
We introduce **_five_ new SSZ transaction types**:
* `SignedLegacyTransaction`
* `SignedEIP2930Transaction`
* `SignedEIP1559Transaction`
* `SignedBasicTransaction`
* `SignedBlobTransaction`
The latter two are the new transaction types introduced by EIP-4844: `SignedBlobTransaction` is the same as before, except it now MUST contain at least one blob, and `SignedBasicTransaction` is the same as the old `SignedBlobTransaction` but with the `blob_versioned_hashes` field removed.
The former three are SSZ representations of the existing three transaction types. For example, `SignedLegacyTransaction` is defined as follows:
```python
class SignedLegacyTransaction(Container):
message: LegacyTransaction
signature: LegacyECDSASignature
class LegacyTransaction(Container):
nonce: uint64
gasprice: uint256
startgas: uint256 # consider uint64?
to: Address
value: uint256
data: ByteList[2**24]
class LegacyECDSASignature(Container):
v: uint8
r: uint256
s: uint256
```
A legacy transaction (of the type available at genesis, or [EIP-155](https://eips.ethereum.org/EIPS/eip-155) transactions, with the chainid encoded into the `v` value) can be converted to a `SignedLegacyTransaction` and back as follows (this is valid executable python code, see test case at the end):
```python
def bti(bytez):
return int.from_bytes(bytez, 'big')
def rlp_to_signed_legacy_transaction(rlp_encoded_tx: bytes):
nonce, gp, sg, to, value, data, v, r, s = rlp.decode(rlp_encoded_tx)
signature = LegacyECDSASignature(v=bti(v), r=bti(r), s=bti(s))
ltx = LegacyTransaction(nonce=bti(nonce), gasprice=bti(gp), startgas=bti(sg),
to=to, value=bti(value), data=data)
return SignedLegacyTransaction(message=ltx, signature=signature)
def signed_legacy_transaction_to_rlp(tx: SignedLegacyTransaction):
nonce = int(tx.message.nonce)
gp = int(tx.message.gasprice)
sg = int(tx.message.startgas)
to = b'' + tx.message.to
value = int(tx.message.value)
data = b'' + tx.message.data
v, r, s = int(tx.signature.v), int(tx.signature.r), int(tx.signature.s)
return rlp.encode([nonce, gp, sg, to, value, data, v, r, s])
```
`SignedEIP2930Transaction` and `SignedEIP1559Transaction` are defined similarly, with similar two-way conversion algorithms from their RLP formats.
### Signature hashes and txids
**We define the `sighash` of a transaction as the hash that a signature is verified against**. For the two new transaction types, `sighash = hash_tree_root(tx.message)`. For the three backwards-compatibility transaction types, `sighash` continues to be defined as before: as the keccak of the RLP encoding of the transaction with the signature values removed.
**We define the `txid` of a transaction as the hash that we recommend public infrastructure (RPC, block explorers...) to use to refer to the hash**. For the two new transactions, `txid = hash_tree_root(tx)`. For the three backwards-compatibility transaction types, `txid = keccak(signed_legacy_transaction_to_rlp(tx))`.
This ensures backwards compatibility for users and applications that depend on the old transaction types. Users that use the old transaction types would still have the same experience as before; their wallets would have to sign the same data as before and they would see the same TXIDs in their dapps, block explorers, etc, as before.
The main difference is that the transactions are stored in a different way in the block data structure (additionally, mempool nodes could accept RLP format, but immediately convert to and propagate SSZ format). As a result, the main place in which backwards compatibility breaks is applications that depend on verifying Merkle proofs in the Merkle patricia tree, or reconstructing the Merkle patricia tree.
### Storage in `ExecutionPayload`
All transactions in a block are stored in the `ExecutionPayload` in one of the new SSZ formats. That is, in the `ExecutionPayload` definition we replace
```python
transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]
```
with
```python
transactions: List[Union[SignedLegacyTransaction, SignedEIP2930Transaction...],
MAX_TRANSACTIONS_PER_PAYLOAD]
```
_Note: the precise definition of `Union` is still a topic of discussion. `Union` is currently not yet used in consensus, and so there remains freedom to decide exactly how `Union` types are to be hash-tree-rooted and serialized. There are different approaches being proposed that attempt to maximize efficiency and simplicity._
_One candidate definition of `Union` is just to define `Union[type1, type2...]` as a `Container` with fields of type `List[type1, 1]`, `List[type2, 1]`, with a restriction that only one of the fields can be nonempty._
### Undecided
* Do we also move receipts to SSZ, to complete the transition to SSZ?
* On the other hand, is there any value in temporarily keeping around an MPT of txs in case some applications use it?
* What exactly is the best way to serialize and hash-tree-root unions?
* Should we not even bother supporting EIP-2930 transactions, given that EIP-1559 transactions supersede it?
* Should we ban nonempty access lists from pre-SSZ transactions? If so, we immediately remove the need to have "full" RLP libraries, as the remaining use cases only need RLP of depth 1, which is a much simpler subset.
* When, if ever, should we expire pre-SSZ transactions outright?
### Appendix: test case for two-way python conversion
Prereqs:
* `import rlp`
* https://github.com/ethereum/consensus-specs/blob/dev/tests/core/pyspec/eth2spec/utils/ssz/ssz_typing.py
* `Address = Bytes20`
Code:
```python
from web3 import Web3
account = Web3().eth.account.from_key(b'\x35' * 32)
tx = account.signTransaction({"nonce": 0, "gas": 21000, "gasPrice": 10**9, "to": "0x" + "35" * 20, "value": 42, "data": "0xf00d"})
rlptx = tx.rawTransaction + b''
# Expect: b'\xf8e\x80\x84;\x9a\xca\x00\x82R\x08\x9455555555555555555555*\x82\xf0\r\x1b\xa0\x06/Z\xd3;_\xceA\xdf.\x05ir\x86c\x90\n\xad\x01\xf6\x01\xbc\xc2z]%\x0e\xb6k3o\xa5\xa0.\xdd\xa9\xe2\xd8?!\xa6\xa2I\r\x9e\x99D\xe2\xb7n\x0f\xc7n\xf2d\x06]\xc5\xff\x87\x85oE\xb6\x06'
z = rlp_to_signed_legacy_transaction(rlptx)
assert signed_legacy_transaction_to_rlp(z) == rlptx
```