Does it make sense to declare separate SSZ type for every Tx type
Overall, even if you have separate SSZ types, you’d still need additional validation to check invariants. For example, to check that the from
address is correct. Or, to check that the blob_versioned_hashes match the blob in the wrapper. Or that the to
field is present for the blob transaction (it’s optional without using blob). Or that the max_fee_per_gas
is >= max_priority_fee_per_gas
. Note you may also need matching TransactionSignatureXyz
, and somehow check that it is compatible with the payload union.
Checking valid field combinations in EIP-6493 is ~20 lines, mostly to ensure that txns retain the limitations from RLP. It could be exhaustively tested. The full list of supported combos is:
TransactionPayloadReplayable
→ original format, not locked to chain ID (no SSZ equivalent)TransactionPayloadLegacyRlp
→ optional to, no access list, no prio fee, no blobTransactionPayloadEip2930Rlp
→ optional to, yes access list, no prio fee, no blobTransactionPayloadEip1559Rlp
→ optional to, yes access list, yes prio fee, no blobTransactionPayloadEip4844Rlp
→ required to, yes access list, yes prio fee, yes blobTransactionPayloadLegacySsz
→ optional to, no access list, no prio fee, no blobTransactionPayloadEip2930Ssz
→ optional to, yes access list, no prio fee, no blobTransactionPayloadEip1559Ssz
→ optional to, yes access list, yes prio fee, no blobTransactionPayloadEip4844Ssz
→ required to, yes access list, yes prio fee, yes blob
There’s the problem of combinatorial explosion. For example, if you want a transaction that has a blob but no priority fee nor access list, and then another one wants no blob but wants a priority fee and no access list, and so on; that’s 8 additional different “tx types” for features that don’t have anything to do with each other. With future features such as multidimensional fees, CREATE2 transaction, different sig_hash mechanisms, and so on, one may want to move towards allowing the signer to pick the combo they want instead of being forced to select a type that supports a superset of what’s needed and then having to trick around with empty lists and default values for all the features they don’t want, like currently done in RLP.
Furthermore, you’d need some mechanism to transfer type information. For example, using an enum prefix similar to Union
. However, that leads to a requirement for the verifier to know about all the enum cases and their meaning. Because new types may be introduced in the future, verifiers can’t become immutable. That’s the case even if they solely care about certain fields of the container; for example, only from
, to
, and value
, and ignore all other fields. On the other hand, with StableContainer
, that could be achieved with a followup proposal like a SparseView
that includes just the bitvectors, the requested 3 fields, plus a merkle proof. The merkle proof shape is statically determinable solely by the bitvectors and the requested fields regardless of tx type, which is not necessarily given by a Union
approach.
About StableUnion
, would be interested to understand what you mean there and how the differences to the StableContainer
are.
I’d also like to better understand more type safe and less error-prone
arguments. In practice, implementations likely go for a single implementation that handles all transactions. Then, for each feature, check if it is used and, if it is, process it. The difference would be that with the TransactionPayloadXyz
jungle you’d need a Generics based implementation that generates another copy of the code for each individual type (feature combination), while with the StableContainer
approach you’d have a single function with runtime checks for all the features. Code size is smaller with the StableContainer
, while the Generics based implementation can exclude certain invalid field combinations (the 20-line check in EIP-6493) in the serialization library rather than its usage. Code size may also have implications on ZK logic based verifiers.