Appearance
Abstract
This EIP defines a migration process of EIP-2718 Recursive-Length Prefix (RLP) transactions to Simple Serialize (SSZ).
Motivation
RLP transactions have a number of shortcomings:
Linear hashing: The signing hash (
sig_hash) and unique identifier (tx_hash) of an RLP transaction are computed by linear keccak256 hashes across its serialization. Even if only partial data is of interest, linear hashes require the full transaction data to be present, including potentially large calldata or access lists. This also applies when computing thefromaddress of a transaction based on thesig_hash.Inefficient inclusion proofs: The Merkle-Patricia Trie (MPT) backing the execution block header's
transactions_rootis constructed from the serialized transactions, internally prepending a prefix to the transaction data before it is keccak256 hashed into the MPT. Due to this prefix, there is no on-chain commitment to thetx_hashand inclusion proofs require the full transaction data to be present.Incompatible representation: As part of the consensus
ExecutionPayload, the RLP serialization of transactions is hashed using SSZ merkleization. These SSZ hashes are incompatible with both thetx_hashand the MPTtransactions_root.Technical debt: All client applications and smart contracts handling RLP transactions have to correctly deal with caveats such as
LegacyTransactionlacking a prefix byte, the inconsistentchain_idandv/y_paritysemantics, and the introduction ofmax_priority_fee_per_gasbetween other fields instead of at the end. As existing transaction types tend to remain valid perpetually, this technical debt builds up over time.Inappropriate opaqueness: The Consensus Layer treats RLP transaction data as opaque, but requires validation of consensus
blob_kzg_commitmentsagainst transactionblob_versioned_hashes, resulting in a more complex than necessary engine API.
This EIP addresses these by defining a lossless conversion mechanism to normalize transaction representation across both Consensus Layer and Execution Layer while retaining support for processing RLP transaction types.
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.
Existing definitions
Definitions from existing specifications that are used throughout this document are replicated here for reference.
| Name | Value |
|---|---|
BYTES_PER_FIELD_ELEMENT | uint64(32) |
FIELD_ELEMENTS_PER_BLOB | uint64(4096) |
| Name | SSZ equivalent |
|---|---|
Hash32 | Bytes32 |
ExecutionAddress | Bytes20 |
VersionedHash | Bytes32 |
KZGCommitment | Bytes48 |
KZGProof | Bytes48 |
Blob | ByteVector[BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB] |
Signatures
Transaction signatures are represented by their native, opaque representation, prefixed by a value indicating their algorithm.
| Name | SSZ equivalent |
|---|---|
ExecutionSignature | ProgressiveByteList |
ExecutionSignatureAlgorithm | uint8 |
Secp256k1
| Name | Value | Description |
|---|---|---|
SECP256K1_ALGORITHM | uint8(0xFF) | Defined from EIP-7932 |
Verification
The algorithm_registry is defined from EIP-7932 along with the calculate_penalty and pubkey_to_address functions.
python
def get_signature_gas_cost(
signature: ExecutionSignature,
expected_algorithm: Optional[ExecutionSignatureAlgorithm]=None
) -> uint:
assert len(signature) > 0
if expected_algorithm is not None:
assert signature[0] == expected_algorithm
return calculate_penalty(signature)
def validate_execution_signature(
signature: ExecutionSignature,
expected_algorithm: Optional[ExecutionSignatureAlgorithm]=None,
):
assert len(signature) > 0
if expected_algorithm is not None:
assert signature[0] == expected_algorithm
assert(signature[0] in algorithm_registry)
def recover_execution_signer(signature: ExecutionSignature, sig_hash: Hash32) -> ExecutionAddress:
assert len(signature) > 0
public_key = algorithm_registry[signature[0]].verify(signature, sig_hash)
return pubkey_to_address(public_key, signature[0])Gas fees
The different kinds of gas fees are combined into a single structure.
| Name | SSZ equivalent | Description |
|---|---|---|
FeePerGas | uint256 | Fee per unit of gas |
python
class BasicFeesPerGas(ProgressiveContainer(active_fields=[1])):
regular: FeePerGas
class BlobFeesPerGas(ProgressiveContainer(active_fields=[1, 1])):
regular: FeePerGas
blob: FeePerGasNormalized transactions
RLP transactions are converted to a normalized SSZ representation. Their original RLP TransactionType is retained to enable recovery of their original RLP representation and associated sig_hash and historical tx_hash values.
| Name | SSZ equivalent | Description |
|---|---|---|
TransactionType | uint8 | EIP-2718 transaction type, range [0x00, 0x7F] |
ChainId | uint256 | EIP-155 chain ID |
GasAmount | uint64 | Amount in units of gas |
Replayable legacy transactions
The original RLP representation of these transactions is replayable across networks with different chain ID.
python
class RlpLegacyReplayableBasicTransactionPayload(
ProgressiveContainer(active_fields=[1, 0, 1, 1, 1, 1, 1, 1])
):
type_: TransactionType # 0x00
nonce: uint64
max_fees_per_gas: BasicFeesPerGas
gas: GasAmount
to: ExecutionAddress
value: uint256
input_: ProgressiveByteList
class RlpLegacyReplayableCreateTransactionPayload(
ProgressiveContainer(active_fields=[1, 0, 1, 1, 1, 0, 1, 1])
):
type_: TransactionType # 0x00
nonce: uint64
max_fees_per_gas: BasicFeesPerGas
gas: GasAmount
value: uint256
input_: ProgressiveByteListEIP-155 legacy transactions
These transactions are locked to a single EIP-155 chain ID.
python
class RlpLegacyBasicTransactionPayload(
ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 1, 1, 1])
):
type_: TransactionType # 0x00
chain_id: ChainId
nonce: uint64
max_fees_per_gas: BasicFeesPerGas
gas: GasAmount
to: ExecutionAddress
value: uint256
input_: ProgressiveByteList
class RlpLegacyCreateTransactionPayload(
ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 0, 1, 1])
):
type_: TransactionType # 0x00
chain_id: ChainId
nonce: uint64
max_fees_per_gas: BasicFeesPerGas
gas: GasAmount
value: uint256
input_: ProgressiveByteListLegacy transactions
python
RlpLegacyTransactionPayload = (
RlpLegacyReplayableBasicTransactionPayload |
RlpLegacyReplayableCreateTransactionPayload |
RlpLegacyBasicTransactionPayload |
RlpLegacyCreateTransactionPayload
)EIP-2930 access list transactions
These transactions support specifying an EIP-2930 access list.
python
class AccessTuple(Container):
address: ExecutionAddress
storage_keys: ProgressiveList[Hash32]
class RlpAccessListBasicTransactionPayload(
ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 1, 1, 1, 1])
):
type_: TransactionType # 0x01
chain_id: ChainId
nonce: uint64
max_fees_per_gas: BasicFeesPerGas
gas: GasAmount
to: ExecutionAddress
value: uint256
input_: ProgressiveByteList
access_list: ProgressiveList[AccessTuple]
class RlpAccessListCreateTransactionPayload(
ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 0, 1, 1, 1])
):
type_: TransactionType # 0x01
chain_id: ChainId
nonce: uint64
max_fees_per_gas: BasicFeesPerGas
gas: GasAmount
value: uint256
input_: ProgressiveByteList
access_list: ProgressiveList[AccessTuple]
RlpAccessListTransactionPayload = (
RlpAccessListBasicTransactionPayload |
RlpAccessListCreateTransactionPayload
)EIP-1559 fee market transactions
These transactions support specifying EIP-1559 priority fees.
python
class RlpBasicTransactionPayload(
ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
):
type_: TransactionType # 0x02
chain_id: ChainId
nonce: uint64
max_fees_per_gas: BasicFeesPerGas
gas: GasAmount
to: ExecutionAddress
value: uint256
input_: ProgressiveByteList
access_list: ProgressiveList[AccessTuple]
max_priority_fees_per_gas: BasicFeesPerGas
class RlpCreateTransactionPayload(
ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 0, 1, 1, 1, 1])
):
type_: TransactionType # 0x02
chain_id: ChainId
nonce: uint64
max_fees_per_gas: BasicFeesPerGas
gas: GasAmount
value: uint256
input_: ProgressiveByteList
access_list: ProgressiveList[AccessTuple]
max_priority_fees_per_gas: BasicFeesPerGas
RlpFeeMarketTransactionPayload = (
RlpBasicTransactionPayload |
RlpCreateTransactionPayload
)EIP-4844 blob transactions
These transactions support specifying EIP-4844 blobs.
python
class RlpBlobTransactionPayload(
ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
):
type_: TransactionType # 0x03
chain_id: ChainId
nonce: uint64
max_fees_per_gas: BlobFeesPerGas
gas: GasAmount
to: ExecutionAddress
value: uint256
input_: ProgressiveByteList
access_list: ProgressiveList[AccessTuple]
max_priority_fees_per_gas: BasicFeesPerGas
blob_versioned_hashes: ProgressiveList[VersionedHash]EIP-7702 set code transactions
These transactions support specifying an EIP-7702 authorization list.
python
class RlpReplayableBasicAuthorizationPayload(ProgressiveContainer(active_fields=[1, 0, 1, 1])):
magic: TransactionType # 0x05
address: ExecutionAddress
nonce: uint64
class RlpBasicAuthorizationPayload(ProgressiveContainer(active_fields=[1, 1, 1, 1])):
magic: TransactionType # 0x05
chain_id: ChainId
address: ExecutionAddress
nonce: uint64
class RlpSetCodeAuthorizationPayload(CompatibleUnion({
0x01: RlpReplayableBasicAuthorizationPayload,
0x02: RlpBasicAuthorizationPayload,
})):
pass
class RlpSetCodeAuthorization(Container):
payload: RlpSetCodeAuthorizationPayload
signature: ExecutionSignature
class RlpSetCodeTransactionPayload(
ProgressiveContainer(active_fields=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1])
):
type_: TransactionType # 0x04
chain_id: ChainId
nonce: uint64
max_fees_per_gas: BasicFeesPerGas
gas: GasAmount
to: ExecutionAddress
value: uint256
input_: ProgressiveByteList
access_list: ProgressiveList[AccessTuple]
max_priority_fees_per_gas: BasicFeesPerGas
authorization_list: ProgressiveList[RlpSetCodeAuthorization]Transaction helpers
python
class TransactionPayload(CompatibleUnion({
0x01: RlpLegacyReplayableBasicTransactionPayload,
0x02: RlpLegacyReplayableCreateTransactionPayload,
0x03: RlpLegacyBasicTransactionPayload,
0x04: RlpLegacyCreateTransactionPayload,
0x05: RlpAccessListBasicTransactionPayload,
0x06: RlpAccessListCreateTransactionPayload,
0x07: RlpBasicTransactionPayload,
0x08: RlpCreateTransactionPayload,
0x09: RlpBlobTransactionPayload,
0x0a: RlpSetCodeTransactionPayload,
})):
pass
class Transaction(Container):
payload: TransactionPayload
signature: ExecutionSignature
class RlpTxType(IntEnum):
LEGACY = 0x00
ACCESS_LIST = 0x01
FEE_MARKET = 0x02
BLOB = 0x03
SET_CODE = 0x04
SET_CODE_MAGIC = 0x05
def calculate_transaction_intrinsic_gas(tx: Transaction) -> uint:
tx_data = tx.payload.data()
gas_cost = TX_BASE_COST # FIXME: Should this be defined from another EIP?
if hasattr(tx_data, "authorization_list"):
for auth in tx_data.authorization_list:
gas_cost += get_signature_gas_cost(auth.signature, expected_algorithm=expected_signature_algorithm)
gas_cost += get_signature_gas_cost(tx.signature, expected_algorithm=expected_signature_algorithm)
return gas_cost
def validate_transaction(tx: Transaction):
tx_data = tx.payload.data()
expected_signature_algorithm = None
if hasattr(tx_data, "type_"):
expected_signature_algorithm = SECP256K1_ALGORITHM
match tx_data.type_:
case RlpTxType.LEGACY:
assert isinstance(tx_data, RlpLegacyTransactionPayload)
case RlpTxType.ACCESS_LIST:
assert isinstance(tx_data, RlpAccessListTransactionPayload)
case RlpTxType.FEE_MARKET:
assert isinstance(tx_data, RlpFeeMarketTransactionPayload)
case RlpTxType.BLOB:
assert isinstance(tx_data, RlpBlobTransactionPayload)
case RlpTxType.SET_CODE:
assert isinstance(tx_data, RlpSetCodeTransactionPayload)
case _:
assert False
if hasattr(tx_data, "authorization_list"):
for auth in tx_data.authorization_list:
auth_data = auth.payload.data()
if hasattr(auth_data, "magic"):
assert auth_data.magic == RlpTxType.SET_CODE_MAGIC
if hasattr(auth_data, "chain_id"):
assert auth_data.chain_id != 0
validate_execution_signature(auth.signature, compute_auth_hash(auth), expected_algorithm=expected_signature_algorithm)
validate_execution_signature(tx.signature, compute_sig_hash(tx), expected_algorithm=expected_signature_algorithm)Execution block header changes
The execution block header's txs-root is transitioned from MPT to SSZ.
python
transactions = ProgressiveList[Transaction](
tx_0, tx_1, tx_2, ...)
block_header.transactions_root = transactions.hash_tree_root()Engine API
In the engine API, the semantics of the transactions field in ExecutionPayload versions adopting this EIP are changed to emit transactions using SSZ serialization.
transactions-ArrayofDATA- Array of transaction objects, each object is a byte list (DATA) representingssz.serialize(tx)
Consensus ExecutionPayload changes
When building a consensus ExecutionPayload, the transactions list is no longer opaque and uses the new Transaction type, aligning the transactions_root across execution blocks and execution payloads.
python
class ExecutionPayload(...):
...
transactions: ProgressiveList[Transaction]
...Unique transaction identifier
For each transaction, an additional identifier tx_root SHALL be assigned:
python
def compute_tx_root(tx: Transaction) -> Hash32:
return Hash32(tx.hash_tree_root())Note that this tx_root differs from the existing tx_hash. Existing APIs based on tx_hash remain available.
Rationale
Forward compatibility
The proposed transaction design is extensible with new fee types, new signature types, and entirely new transaction features (e.g., CREATE2), while retaining compatibility with the proposed transactions.
Verifier improvements
The transactions_root is effectively constructed from the list of tx_root, enabling transaction inclusion proofs. Further, partial data becomes provable, such as destination / amount without requiring the full calldata. This can reduce gas cost or zk proving cost when verifying L2 chain data in an L1 smart contract.
Consensus client improvements
Consensus Layer implementations may drop invalid blocks early if consensus blob_kzg_commitments do not validate against transaction blob_versioned_hashes and no longer need to query the Execution Layer for that validation. Future versions of the engine API could be simplified to drop the transfers of blob_kzg_commitments to the EL.
Backwards Compatibility
Applications that rely on the replaced MPT transactions_root in the block header require migration to the SSZ transactions_root.
While there is no on-chain commitment of the tx_hash, it is widely used in JSON-RPC and the Ethereum Wire Protocol to uniquely identify transactions. The conversion from RLP transactions to SSZ is lossless. The original RLP sig_hash and tx_hash can be recovered from the SSZ representation.
RLP and SSZ transactions may clash when encoded. It is essential to use only a single format within one channel.
Security Considerations
None
Copyright
Copyright and related rights waived via CC0.