EIP-2972: Wrapped Legacy Transactions
Simple Summary
Two new transaction types for wrapping legacy transactions with and without a chain ID.
Abstract
Introduces two new EIP-2718 transactions that are signature compatible with legacy transactions and can be automatically upgraded by any client.
0x00 || ssz.serialize(yParity, r, s, rlp([nonce, gasPrice, gasLimit, to, value, data]))
0x01 || ssz.serialize(yParity, r, s, rlp([nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]))
Motivation
We would like to eventually deprecate legacy transactions so we no longer have to retain code in the networking and signer layer that deals with them. However, we also want to ensure that signatures for transactions that were generated prior to that deprecation are still valid and funds don't end up stuck because of an inability to sign a new style transaction. This EIP provides a mechanism for transmitting/including transactions in a way that is EIP-2718 compatible while still being signature compatible with legacy transactions.
Specification
Definitions
||
is the byte/byte-array concatenation operator.yParity
is the parity (0 for even, 1 for odd) of they
value of the curve point for whichr
is thex
value in the secp256k1 signing process.
Transactions
As of FORK_BLOCK_NUMBER
, 0x00 || ssz.serialize(yParity, r, s, rlp([nonce, gasPrice, gasLimit, to, value, data]))
will be a valid transaction where:
- the RLP encoded transaction portion is signed/processed/handled exactly the same as legacy transactions were signed/processed/handled, with the exception of the final encoding
- TODO: Hashing or Merkleizing for block transaction root
As of FORK_BLOCK_NUMBER
, 0x01 || ssz.serialize(yParity, r, s, rlp([nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]))
will be a valid transaction where:
- the RLP encoded transaction portion is signed/processed/handled exactly the same as legacy transactions were signed/processed/handled, with the exception of the final encoding
- TODO: Hashing or Merkleizing for block transaction root
The SSZ schema for both transaction types is:
Note: sszencode(yParity, r, s, rlp(...))
is the same as yParity || r || s || 0x45000000 || rlp(...)
As of FORK_BLOCK_NUMBER
, rlp(nonce, gasPrice, gasLimit, to, value, data, v, r, s)
will no longer be a valid transaction in a block.
Receipts
As of FORK_BLOCK_NUMBER
, 0 || ssz.serialize(status, cumulativeGasUsed, logsBloom, logs)
will be a valid receipt where:
- the
ReceiptPayload
will be generated/processed/handled exactly the same as legacy receipts were processed/handled with the exception of its encoding - TODO: Hashing or Merkleizing for block receipt root
As of FORK_BLOCK_NUMBER
, 1 || ssz.serialize(status, cumulativeGasUsed, logsBloom, logs)
will be a valid receipt where:
- the
ReceiptPayload
will be generated/processed/handled exactly the same as legacy receipts were processed/handled with the exception of its encoding - TODO: Hashing or Merkleizing for block receipt root
The SSZ schema for both receipt types is:
As of FORK_BLOCK_NUMBER
, rlp(status, cumulativeGasUsed, logsBloom, logs)
will no longer be a valid receipt in a block.
Rationale
Signature doesn't include transaction type as first signature byte
These transaction types are explicitly designed to be signature compatible with legacy transactions, which means we cannot change the data being signed. See Security Considerations section for more details.
Two transaction types instead of one
With the introduction of typed transactions, we no longer need to do bit packing to avoid changing the shape of the signature.
Legacy transactions introduced chain ID in EIP-155 and wanted to avoid changing the transaction array length, so it bitpacked the chainID into the signature's v
value.
Since we no longer need to guarantee consistent payload lengths between transaction types, we have opted to have two transaction types with clear fields.
Signature separate from signed data
When validating a signature, one must first separate out the signed data from the signature and then validate the signature against the signed data.
In the case of legacy transactions, this was a bit of a burden since you had to first RLP decode the transaction, then extract out the signature, then RLP encode a subset of the transaction.
EIP-155 made this process even worse by requiring the validator to further decode the v
signature value to extract the chain ID (if present) and include that in the signed data payload.
By having the signed data encoded exactly as it is signed, we make it so one can verify the transaction's signature without having to do any decoding before hand.
By having the signature SSZ encoded up front, we can easily extract the signature without even having to use a decoder.
SSZ for serialization
There is a weak consensus that RLP is not a particularly good encoding scheme for hashed data partially due to its inability to be streamed.
SSZ is almost certainly going to be included in Ethereum at some point in the future, so clients likely have access to an SSZ decoder.
For this particular case, manual decoding without a full SSZ decoder isn't too complicated, though it does require doing a bit of "pointer math" since logs
is an array of variable length items.
Deprecating legacy transactions
By deprecating legacy transactions, we make it easier for clients as they can always deal with typed transactions in blocks.
Max length of logs and logs data
EIP-706 limits devp2p messages to 24 bit length, which gives us a pragmatic cap at that for any single transaction at the moment. This number seems to far exceed what is reasonable anytime in the near future, so feels like as reasonable of a cap as any.
Backwards Compatibility
The new transactions are signature compatible with legacy transactions. Legacy transactions can be decoded and then encoded as type 0 or type 1 transactions. This EIP does not introduce any deprecation process for legacy encoded transactions, though the authors do encourage client developers to upgrade legacy encoded transactions to typed transactions as soon as it is reasonable.
The signature compatibility means that a client may see the same transaction encoded both ways. In such a case the client can choose which to retain, but it is encouraged to retain the typed transaction rather than the legacy encoded transaction. Since the two transactions would share a nonce, only one will ever be valid in a chain at a time.
Test Cases
TBD
Implementation
TBD
Security Considerations
While EIP-2718 strongly recommends including the transaction type as the first byte of the signed data, we cannot accomplish that in this case because we need to remain signature compatible with legacy transactions.
Luckily, EIP-2718 also excludes transaction types 0xc0
to 0xfe
from valid transaction types, and the first byte of the signature in this case is in that range so we can be sure this will not conflict with any future transaction types.
A signature for these transaction types does collide with legacy transactions, but the transactions will be processed the same so it doesn't matter if the transaction ends up included as a legacy transaction or a typed transaction.
Copyright
Copyright and related rights waived via CC0.