This EIP defines a migration process of EIP-2718 Recursive-Length Prefix (RLP) transactions to Simple Serialize (SSZ).
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 the from
address of a transaction based on the sig_hash
.
Inefficient inclusion proofs: The Merkle-Patricia Trie (MPT) backing the execution block header's transactions_root
is 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 the tx_hash
and 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 the tx_hash
and the MPT transactions_root
.
No extensibility: Transaction types cannot be extended with optional features. Hypothetically, if EIP-4844 blob transactions existed from the start, new features such as EIP-2930 access lists and EIP-1559 priority fees would have required two new transacion types each to extend both the basic and blob transaction types.
Technical debt: All client applications and smart contracts handling RLP transactions have to correctly deal with caveats such as LegacyTransaction
lacking a prefix byte, the inconsistent chain_id
and v
/ y_parity
semantics, and the introduction of max_priority_fee_per_gas
between 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_commitments
against transaction blob_versioned_hashes
, resulting in a more complex than necessary engine API.
This EIP defines a lossless conversion mechanism to normalize transaction representation across both Consensus Layer and Execution Layer while retaining support for processing RLP transaction types.
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.
Definitions from existing specifications that are used throughout this document are replicated here for reference.
Name | Value |
---|---|
MAX_TRANSACTIONS_PER_PAYLOAD |
uint64(2**20) (= 1,048,576) |
BYTES_PER_FIELD_ELEMENT |
uint64(32) |
FIELD_ELEMENTS_PER_BLOB |
uint64(4096) |
MAX_BLOB_COMMITMENTS_PER_BLOCK |
uint64(2**12) (= 4,096) |
Name | SSZ equivalent |
---|---|
Hash32 |
Bytes32 |
ExecutionAddress |
Bytes20 |
VersionedHash |
Bytes32 |
KZGCommitment |
Bytes48 |
KZGProof |
Bytes48 |
Blob |
ByteVector[BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB] |
ExecutionSignature
containerSignatures use their native, opaque representation, and are extended with an on-chain commitment to the signing address.
Name | Value | Description |
---|---|---|
SECP256K1_SIGNATURE_SIZE |
32 + 32 + 1 (= 65) |
Byte length of a secp256k1 ECDSA signature |
MAX_EXECUTION_SIGNATURE_FIELDS |
uint64(2**3) (= 8) |
Maximum number of fields to which ExecutionSignature can ever grow in the future |
class ExecutionSignature(StableContainer[MAX_EXECUTION_SIGNATURE_FIELDS]):
secp256k1: Optional[ByteVector[SECP256K1_SIGNATURE_SIZE]]
class Secp256k1ExecutionSignature(Profile[ExecutionSignature]):
secp256k1: ByteVector[SECP256K1_SIGNATURE_SIZE]
def secp256k1_pack(r: uint256, s: uint256, y_parity: uint8) -> ByteVector[SECP256K1_SIGNATURE_SIZE]:
return r.to_bytes(32, 'big') + s.to_bytes(32, 'big') + bytes([y_parity])
def secp256k1_unpack(signature: ByteVector[SECP256K1_SIGNATURE_SIZE]) -> tuple[uint256, uint256, uint8]:
r = uint256.from_bytes(signature[0:32], 'big')
s = uint256.from_bytes(signature[32:64], 'big')
y_parity = signature[64]
return (r, s, y_parity)
def secp256k1_validate(signature: ByteVector[SECP256K1_SIGNATURE_SIZE]):
SECP256K1N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
r, s, y_parity = secp256k1_unpack(signature)
assert 0 < r < SECP256K1N
assert 0 < s <= SECP256K1N // 2
assert y_parity in (0, 1)
def secp256k1_recover_signer(signature: ByteVector[SECP256K1_SIGNATURE_SIZE],
sig_hash: Hash32) -> ExecutionAddress:
ecdsa = ECDSA()
recover_sig = ecdsa.ecdsa_recoverable_deserialize(signature[0:64], signature[64])
public_key = PublicKey(ecdsa.ecdsa_recover(sig_hash, recover_sig, raw=True))
uncompressed = public_key.serialize(compressed=False)
return ExecutionAddress(keccak(uncompressed[1:])[12:])
Transaction
containerAll transactions are represented as a single, normalized SSZ container. The definition uses the StableContainer[N]
SSZ type and Optional[T]
as defined in EIP-7495.
Name | Value | Description |
---|---|---|
MAX_FEES_PER_GAS_FIELDS |
uint64(2**4) (= 16) |
Maximum number of fields to which FeesPerGas can ever grow in the future |
MAX_CALLDATA_SIZE |
uint64(2**24) (= 16,777,216) |
Maximum input calldata byte length for a transaction |
MAX_ACCESS_LIST_STORAGE_KEYS |
uint64(2**19) (= 524,288) |
Maximum number of storage keys within an access tuple |
MAX_ACCESS_LIST_SIZE |
uint64(2**19) (= 524,288) |
Maximum number of access tuples within an access_list |
MAX_AUTHORIZATION_PAYLOAD_FIELDS |
uint64(2**4) (= 16) |
Maximum number of fields to which AuthorizationPayload can ever grow in the future |
MAX_AUTHORIZATION_LIST_SIZE |
uint64(2**16) (= 65,536) |
Maximum number of authorizations within an authorization_list |
MAX_TRANSACTION_PAYLOAD_FIELDS |
uint64(2**5) (= 32) |
Maximum number of fields to which TransactionPayload can ever grow in the future |
Name | SSZ equivalent | Description |
---|---|---|
TransactionType |
uint8 |
EIP-2718 transaction type, range [0x00, 0x7F] |
ChainId |
uint64 |
EIP-155 chain ID |
FeePerGas |
uint256 |
Fee per unit of gas |
GasAmount |
uint64 |
Amount in units of gas |
class FeesPerGas(StableContainer[MAX_FEES_PER_GAS_FIELDS]):
regular: Optional[FeePerGas]
# EIP-4844
blob: Optional[FeePerGas]
class AccessTuple(Container):
address: ExecutionAddress
storage_keys: List[Hash32, MAX_ACCESS_LIST_STORAGE_KEYS]
class AuthorizationPayload(StableContainer[MAX_AUTHORIZATION_PAYLOAD_FIELDS]):
magic: Optional[TransactionType]
chain_id: Optional[ChainId]
address: Optional[ExecutionAddress]
nonce: Optional[uint64]
class Authorization(Container):
payload: AuthorizationPayload
signature: ExecutionSignature
class TransactionPayload(StableContainer[MAX_TRANSACTION_PAYLOAD_FIELDS]):
# EIP-2718
type_: Optional[TransactionType]
# EIP-155
chain_id: Optional[ChainId]
nonce: Optional[uint64]
max_fees_per_gas: Optional[FeesPerGas]
gas: Optional[GasAmount]
to: Optional[ExecutionAddress]
value: Optional[uint256]
input_: Optional[ByteList[MAX_CALLDATA_SIZE]]
# EIP-2930
access_list: Optional[List[AccessTuple, MAX_ACCESS_LIST_SIZE]]
# EIP-1559
max_priority_fees_per_gas: Optional[FeesPerGas]
# EIP-4844
blob_versioned_hashes: Optional[List[VersionedHash, MAX_BLOB_COMMITMENTS_PER_BLOCK]]
# EIP-7702
authorization_list: Optional[List[Authorization, MAX_AUTHORIZATION_LIST_SIZE]]
class Transaction(Container):
payload: TransactionPayload
signature: ExecutionSignature
Transaction
profilesEIP-7495 Profile
definitions provide type safety for valid transactions. Their original RLP TransactionType
is retained to enable recovery of their original RLP representation and associated sig_hash
and tx_hash
values where necessary.
class BasicFeesPerGas(Profile[FeesPerGas]):
regular: FeePerGas
class BlobFeesPerGas(Profile[FeesPerGas]):
regular: FeePerGas
blob: FeePerGas
class RlpLegacyTransactionPayload(Profile[TransactionPayload]):
type_: TransactionType
chain_id: Optional[ChainId]
nonce: uint64
max_fees_per_gas: BasicFeesPerGas
gas: GasAmount
to: Optional[ExecutionAddress]
value: uint256
input_: ByteList[MAX_CALLDATA_SIZE]
class RlpLegacyTransaction(Container):
payload: RlpLegacyTransactionPayload
signature: Secp256k1ExecutionSignature
class RlpAccessListTransactionPayload(Profile[TransactionPayload]):
type_: TransactionType
chain_id: ChainId
nonce: uint64
max_fees_per_gas: BasicFeesPerGas
gas: GasAmount
to: Optional[ExecutionAddress]
value: uint256
input_: ByteList[MAX_CALLDATA_SIZE]
access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE]
class RlpAccessListTransaction(Container):
payload: RlpAccessListTransactionPayload
signature: Secp256k1ExecutionSignature
class RlpFeeMarketTransactionPayload(Profile[TransactionPayload]):
type_: TransactionType
chain_id: ChainId
nonce: uint64
max_fees_per_gas: BasicFeesPerGas
gas: GasAmount
to: Optional[ExecutionAddress]
value: uint256
input_: ByteList[MAX_CALLDATA_SIZE]
access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE]
max_priority_fees_per_gas: BasicFeesPerGas
class RlpFeeMarketTransaction(Container):
payload: RlpFeeMarketTransactionPayload
signature: Secp256k1ExecutionSignature
class RlpBlobTransactionPayload(Profile[TransactionPayload]):
type_: TransactionType
chain_id: ChainId
nonce: uint64
max_fees_per_gas: BlobFeesPerGas
gas: GasAmount
to: ExecutionAddress
value: uint256
input_: ByteList[MAX_CALLDATA_SIZE]
access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE]
max_priority_fees_per_gas: BlobFeesPerGas
blob_versioned_hashes: List[VersionedHash, MAX_BLOB_COMMITMENTS_PER_BLOCK]
class RlpBlobTransaction(Container):
payload: RlpBlobTransactionPayload
signature: Secp256k1ExecutionSignature
class RlpSetCodeAuthorizationPayload(Profile[AuthorizationPayload]):
magic: TransactionType
chain_id: Optional[ChainId]
address: ExecutionAddress
nonce: uint64
class RlpSetCodeAuthorization(Container):
payload: RlpSetCodeAuthorizationPayload
signature: Secp256k1ExecutionSignature
class RlpSetCodeTransactionPayload(Profile[TransactionPayload]):
type_: TransactionType
chain_id: ChainId
nonce: uint64
max_fees_per_gas: BasicFeesPerGas
gas: GasAmount
to: ExecutionAddress
value: uint256
input_: ByteList[MAX_CALLDATA_SIZE]
access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE]
max_priority_fees_per_gas: BasicFeesPerGas
authorization_list: List[RlpSetCodeAuthorization, MAX_AUTHORIZATION_LIST_SIZE]
class RlpSetCodeTransaction(Container):
payload: RlpSetCodeTransactionPayload
signature: Secp256k1ExecutionSignature
Helpers are provided to identify the EIP-7495 Profile
of a normalized Transaction
. The type system ensures that all required fields of the Profile
are present and that excluded fields are absent.
LEGACY_TX_TYPE = TransactionType(0x00)
ACCESS_LIST_TX_TYPE = TransactionType(0x01)
FEE_MARKET_TX_TYPE = TransactionType(0x02)
BLOB_TX_TYPE = TransactionType(0x03)
SET_CODE_TX_TYPE = TransactionType(0x04)
SET_CODE_TX_MAGIC = TransactionType(0x05)
def identify_authorization_profile(auth: Authorization) -> Type[Profile]:
if auth.payload.magic == SET_CODE_TX_MAGIC:
if auth.payload.chain_id == 0:
raise Exception(f'Unsupported chain ID in Set Code RLP authorization: {auth}')
return RlpSetCodeAuthorization
raise Exception(f'Unsupported authorization: {auth}')
def identify_transaction_profile(tx: Transaction) -> Type[Profile]:
if tx.payload.type_ == SET_CODE_TX_TYPE:
for auth in tx.payload.authorization_list or []:
auth = identify_authorization_profile(auth).from_base(auth)
if not isinstance(auth, RlpSetCodeAuthorization):
raise Exception(f'Unsupported authorization in Set Code RLP transaction: {tx}')
return RlpSetCodeTransaction
if tx.payload.type_ == BLOB_TX_TYPE:
if (tx.payload.max_priority_fees_per_gas or FeesPerGas()).blob != 0:
raise Exception(f'Unsupported blob priority fee in Blob RLP transaction: {tx}')
return RlpBlobTransaction
if tx.payload.type_ == FEE_MARKET_TX_TYPE:
return RlpFeeMarketTransaction
if tx.payload.type_ == ACCESS_LIST_TX_TYPE:
return RlpAccessListTransaction
if tx.payload.type_ == LEGACY_TX_TYPE:
return RlpLegacyTransaction
raise Exception(f'Unsupported transaction: {tx}')
To obtain a transaction's from
address, its identifier, or an authorization's authority
address, see EIP assets for a definition of compute_sig_hash
, compute_tx_hash
, and compute_auth_hash
that account for the various transaction types.
The execution block header's txs-root
is transitioned from MPT to SSZ.
transactions = List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD](
tx_0, tx_1, tx_2, ...)
block_header.transactions_root = transactions.hash_tree_root()
In the engine API, the structure of the transactions
field in ExecutionPayload
versions adopting this EIP is changed from Array of DATA
to Array of TransactionV1
.
TransactionV1
is defined to map onto the SSZ Transaction
type, as follows:
payload
: TransactionPayloadV1
- An OBJECT
containing the fields of a TransactionPayloadV1
structuresignature
: ExecutionSignatureV1
- An OBJECT
containing the fields of an ExecutionSignatureV1
structureTransactionPayloadV1
is defined to map onto the SSZ TransactionPayload
StableContainer
, as follows:
type
: QUANTITY|null
, 8 Bits or null
chainId
: QUANTITY|null
, 256 Bits or null
nonce
: QUANTITY|null
, 64 Bits or null
maxFeesPerGas
: FeesPerGasV1|null
- An OBJECT
containing the fields of a FeesPerGasV1
structure or null
gas
: QUANTITY|null
, 64 Bits or null
to
: DATA|null
, 20 Bytes or null
value
: QUANTITY|null
, 256 Bits or null
input
: DATA|null
, 0 through MAX_CALLDATA_SIZE
bytes or null
accessList
: Array of AccessTupleV1
- 0 through MAX_ACCESS_LIST_SIZE
OBJECT
entries each containing the fields of an AccessTupleV1
structure, or null
maxPriorityFeesPerGas
: FeesPerGasV1|null
- An OBJECT
containing the fields of a FeesPerGasV1
structure or null
blobVersionedHashes
: Array of DATA|null
- 0 through MAX_BLOB_COMMITMENTS_PER_BLOCK
DATA
entries each containing 32 Bytes, or null
authorizationList
: Array of AuthorizationV1
- 0 through MAX_AUTHORIZATION_LIST_SIZE
OBJECT
entries each containing the fields of an AuthorizationV1
structure, or null
FeesPerGasV1
is defined to map onto the SSZ FeesPerGas
StableContainer
, as follows:
regular
: QUANTITY|null
, 256 Bits or null
blob
: QUANTITY|null
, 256 Bits or null
AccessTupleV1
is defined to map onto the SSZ AccessTuple
Container
, as follows:
address
: DATA
, 20 BytesstorageKeys
: Array of DATA
- 0 through MAX_ACCESS_LIST_STORAGE_KEYS
DATA
entries each containing 32 BytesAuthorizationV1
is defined to map onto the SSZ Authorization
Container
, as follows:
payload
: AuthorizationPayloadV1
- An OBJECT
containing the fields of an AuthorizationPayloadV1
structuresignature
: ExecutionSignatureV1
- An OBJECT
containing the fields of an ExecutionSignatureV1
structureAuthorizationPayloadV1
is defined to map onto the SSZ AuthorizationPayload
StableContainer
, as follows:
magic
: QUANTITY|null
, 8 Bits or null
chainId
: QUANTITY|null
, 256 Bits or null
address
: DATA|null
, 20 Bytes or null
nonce
: QUANTITY|null
, 64 Bits or null
ExecutionSignatureV1
is defined to map onto the SSZ ExecutionSignature
StableContainer
, as follows:
secp256k1
: DATA|null
, 65 Bytes or null
ExecutionPayload
changesWhen building a consensus ExecutionPayload
, the transactions
list is no longer opaque and uses the new Transaction
type.
class ExecutionPayload(Container):
...
transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]
...
PooledTransaction
containerDuring transaction gossip responses (PooledTransactions
), each Transaction
is wrapped into a PooledTransaction
.
Name | Value | Description |
---|---|---|
MAX_POOLED_TRANSACTION_FIELDS |
uint64(2**3) (= 8) |
Maximum number of fields to which PooledTransaction can ever grow in the future |
class BlobData(Container):
blobs: List[Blob, MAX_BLOB_COMMITMENTS_PER_BLOCK]
commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]
proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK]
class PooledTransaction(StableContainer[MAX_POOLED_TRANSACTION_FIELDS]):
tx: Optional[Transaction]
blob_data: Optional[BlobData]
The additional validation constraints defined in EIP-4844 also apply to transactions that define tx.payload.blob_versioned_hashes
or blob_data
.
The semantics of the types
element in transaction gossip announcements (NewPooledTransactionHashes
) are changed to match ssz(PooledTransaction.active_fields())
. The separate control flow for fetching blob transactions compared to basic transactions is retained.
Note that this change maps active_fields
for PooledTransaction
with blob_data
to 0x03
, which coincides with the previous BLOB_TX_TYPE
prefix of blob RLP transactions.
When exchanging SSZ transactions via the Ethereum Wire Protocol, the following EIP-2718 compatible envelopes are used:
Name | Value | Description |
---|---|---|
SSZ_TX_TYPE |
TransactionType(0x1f) |
Endpoint specific SSZ object |
Transaction
: SSZ_TX_TYPE || snappyFramed(ssz(Transaction))
PooledTransaction
: SSZ_TX_TYPE || snappyFramed(ssz(PooledTransaction))
Objects are encoded using SSZ and compressed using the Snappy framing format, matching the encoding of consensus objects as defined in the consensus networking specification. As part of the encoding, the uncompressed object length is emitted; the RECOMMENDED limit to enforce per object is MAX_CHUNK_SIZE
bytes.
Implementations SHOULD continue to support accepting RLP transactions into their transaction pool. However, such transactions MUST be converted to SSZ for inclusion into an ExecutionPayload
. See EIP assets for a reference implementation to convert from RLP to SSZ, as well as corresponding test cases. The original sig_hash
and tx_hash
are retained throughout the conversion process.
Switching to a single, unified and forward compatible transaction format within execution blocks reduces implementation complexity for client applications and smart contracts. Future use cases such as transaction inclusion proofs or submitting individual verifiable chunks of calldata to a smart contract become easier to implement with SSZ.
Various protocol inefficiencies are also addressed. While the transaction data is hashed several times under the RLP system, including (1) sig_hash
, (2) tx_hash
, (3) MPT internal hash, and (4) SSZ internal hash, the normalized representation reduces the hash count. Furthermore, 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 block hash validation.
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 tx_hash
remains stable across the conversion from RLP to SSZ.
The conversion from RLP transactions to SSZ is lossless. The original RLP sig_hash
and tx_hash
can be recovered from the SSZ representation.
None
Copyright and related rights waived via CC0.