EIP-2718: Typed Transaction Envelope

After sleeping on it and reading the feedback from @matt I have removed the ORIGIN and CALLER stuff. I added a note in the rationale saying that ORIGIN and CALLER should be the same for the first frame of the transaction for all transaction types, and that if future transaction types want to include additional data they will need a new opcode.

I am mildly convinced that allowing differentiation by transaction type may lead to some bad things like contracts not working for people who utilize certain types of transactions, but in that case I’m not sure how to best deal with sponsored transactions. I’ll continue the discussion on that over in EIP-2711: Separate gas payer from msg.sender

1 Like

Not sure I’m knowledgable enough to comment on this EIP’s worth, but I noticed a few small issue with wording:

In the rationale section, under “Opaque second item rather than an array” section you say,

By having the second item of the array just be opaque bytes, rather than a list, we can support different encoding formats for the transaction payload in the future, such as SSZ or a fixed-width format.

In the backward compatibility section you say:

...noting that the second element is a list rather than a value.

Did you mean that the second item is bytes?

And in the Security Considerations section you say:

...the second item as a value when it is encoded as an array

Probably a result of the change to bytes after the initial writing of the spec.

Thought I’d point that out as it’s a bit confusing…

@tjayrush Both of those were mistakes due to a change from earlier version. Both have been fixed!


Sorry if I missed this in the docs, does each transaction type get its own mempool?

It’s not clear what you mean by “get its own mempool”. If you mean the mempool may need to maintain a list of transactions of a certain type to perform additional checks (e.g. that their total gas is less than the allow 1559 limits or that their valid_until block hasn’t lapsed), then I suppose the answer is yes. Whether or not these checks are performed in parallel seems like an implementation concern.

That is “out of scope” of this EIP, but for the currently on-deck 2718 transaction types, 1559 is the only one that would need its own mempool. The rest would share one with legacy transactions.

Out of scope makes sense, but it seems like there would be plenty of situations where we’d want a smooth transition from one tx type to another.

As an administrative matter, these “parsimonious changes” are a lot more acceptable to me if these sorts of major updates were to actually happen multiple times a year, which is a bit of a catch-22.

In the 1559 case, we have two mempools but they aren’t intended to live side-by-side forever. The intent is that one eventually replaces the other. This is a bit different from other new transaction types where the intent is that they live side-by-side forever. If we imagine 1559 landing after 2711 and other new transaction types, I suspect 1559 will need to actually replace all transaction types with new transaction types that include the new 1559 gas semantics. For example, if we have transaction type 0 (legacy) and transaction type 1 (sponsored/batch/expiring transacitons) when 1559 lands, then 1559 would need to introduce two new types: 2 (legacy with 1559 semantics) and 3 (sponsored/batch/expiring with 1559 semantics).

Questions that I think we would need to answer to move forward:

Do we think that switching mempools is a common enough operation that it is worth trying to generalize a solution? Do we think that should be part of 2718, or should it be part of a separate EIP that defines a mechanism for dealing with pool transitions? Will we always want to go from one mempool to another, or are there situations where we may want multiple side-by-side mempools indefinitely?

Maybe I missed this somewhere in all the text above here… But if the format is rlp([0, rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s])]) . The signed data is bundled in the inner rlp. So a wrapped transaction can be re-wrapped with some other format? How would you uniquely identify a transaction? The hash of the inner payload, or the hash of the (unsigned) wrapping?

Good question, and the EIP does need to be updated to specify what is hashed for the unique transaction identifier. My initial thinking is that we should identify the transaction by the wrapped hash. However, that would mean that on the fork block when all transactions in the pending queue are wrapped (a one-time operation), their hashes would all change which will almost certainly break any dapps running during the transition, and probably break a lot of user interfaces around that time. The situation wouldn’t be unrecoverable, but it definitely could be messy.

We could use the inner transaction hash, but long term this feels dirty to me as every other transaction type going forward will (hopefully) be identified by a full hash of the transaction, and we’ll forever be left with this one oddball situation to deal with.

Perhaps as a mechanism to protect dapps operating during the transition, clients could have some range of blocks over which transactions have two unique identifiers (hash of inner and hash of outer) such that when someone looks up either with the client, the client will return the details requested for that transaction. Since this would just be a feature for dealing with a transient problem, the code for this (and any related DB entries) could eventually be deleted, it would only have to exist for some finite period of time around the fork block. We just want to make sure that most transactions that were in the pending queue on fork block are accessible by either old or new transaction hash, even though they were mined after the fork block.

Thoughts? Core dev thoughts on the subject would be particularly valuable as it would help provide insight into how realistic either solution is.

1 Like

Well, all that juggling just to handle a temporary UX-cornercase around the actual fork block seems not worth it, IMO. I hadn’t read the EIP properly, and thought that both old-style and ‘wrapped’ txs were allowed.

As I see it, it’s very odd to sign something, and have the ‘wrapping’ not be part of the signed stuff. So my gut feelings are

  • The signature should encompass the wrapping,
  • The hash should be a hash of the whole wrapped package

But with that, we have to break up the wrapping, since the inner part now must know about the outer part…?

There are two separate problems that I think you may be conflating @holiman:

  1. Signing the wrapped transaction vs signing only the inner transaction for type 0 transactions.
  2. Hashing the wrapped transaction vs hashing the inner transaction for type 0 transactions.

If we change how transactions are signed, then every single wallet will break (be unable to sign transactions) as of the fork block until it is updated. By signing only the inner transaction, wallets can continue to sign the same thing they always signed and the client they communicate with (e.g., Geth) can just wrap them up.

If we change how transactions are hashed, then dapps will break if they submit a transaction before the fork block and it is mined after the fork block. In almost all cases, this can probably be resolved by the end-user by refreshing the page (and possibly clearing their local browser cache, depending on the specifics of the dapp).

I think I can get on board with just eating the transient problem with the hashes changes around the fork block. I don’t think I can get on board with having all signing tools breaking until updated (this would include all hardware wallets, offline wallets, etc. I believe). Changing the signature would also break anyone who has a pre-signed transaction sitting around (e.g., a paper asset recovery transaction for a cold wallet).

In a perfect world I agree that the signature should sign the envelope (including transaction type) and the hash should be of the whole thing. I just don’t think we can reasonably achieve the former is all.

I have updated 2718 to include specification on hashing (hash the envelope).

I have also added some recommendations for client developers (wrap transactions just before fork block and provide access to transactions by both hashes for a time) but neither are MUST, just SHOULD so there is no requirement if client developers think it isn’t worth the effort.

I also added some rational for signing only the Payload for type 0 transactions and hashing the outer transaction.

Transaction Receipts

In 2711, we introduce batch transactions with a single gas payer. There will be a single transaction hash for the whole thing, so that leads me to believe that means there will be a single receipt for the whole thing. However, there may be multiple sub-transactions that are part of that receipt, each of which can succeed/fail independently.

We could assert that batch transactions must all succeed, or all fail as part of 2711, which lets us get away with a single status code, but what about future transaction types that may not adhere to that rule? Another option would be to have the status code field be a uint256, where each bit represents the success of an inner transaction in the batch (putting a limit of 256 transactions per batch), but again that feels like it is tied pretty tightly with 2711 specifically and doesn’t generalize well (what if you have a transaction tree?).

Transaction receipts also currently have a from , to , contractAddress in them, as well as a logs array. from and to will need to be changed to something else or removed I think. We could put the gas payer in for from in theory, and technically we don’t need to separate out the logs by sub-transaction (can just have one big logs array for all nested transactions).

This all is making me wonder if we should version transaction receipts as well? A more useful transaction receipt would have something like childReceipts which contains an array of sub-receipts where each sub-receipt had a from, to, contractAddress, status field in it. If we do version transaction receipts, it feels like we should do it in 2711 so that receipt types align with transaction types, so each transaction type would define both the transaction payload and the transaction receipt payloads.

I’m looking for feedback/thoughts on how to handle receipts for different transaction types. My current leaning is to version receipts and couple the version with transaction types (so an EIP that defines a new transaction type would also define a new receipt type).

I have updated 2718 to now include information on how receipts should be enveloped. I went with the typed receipts solution with the type number matching the transaction type, so it is up to each new transaction type to define its receipts.

I like this standard a lot and if i am to add my won 2cents - we should avoid numbers as the version. They should be descriptive of what they are. The data segment is opaque, so the identifier is an enum and making it a letter instead of a number maybe more practical.

I wrote a EIP-2718 envelope type for ditto transactions, and i used the letter ‘d’ so that it is evident that it is more evident that it is a ditto transaction vs an arbitrary auto-incrementing number.

The reason numbers are used instead of a more human readable mechanism has to do with the way transactions are encoded on the wire. The smaller the number is, the fewer bytes it consumes. For numbers between 0 and 127 the value only takes up one byte when encoded. This means we have fairly little space to work with before we start needing more bytes. If we used a single ASCII encoded letter I don’t think readability would be improved, but it may become hard to keep track of which values have been used and which haven’t.

1 Like

That makes sense. So, I also see that in RLP-encoded a single byte between 0x00, 0x7f is the explicit byte resulting in a single-byte keyspace of 128.( https://medium.com/coinmonks/data-structure-in-ethereum-episode-1-recursive-length-prefix-rlp-encoding-decoding-d1016832f919 ). On that note, there are only 127 printable ASCII codes. What do you think of using 7-bit ASCII characters as the version param? So, for example I want to create a new “ditto transaction” type if I wanted to use ‘d’ for ditto transactions which would be ascii 0x44 that fits within the single byte range of RLP.

I don’t believe that a single ASCII encoded character is any more human readable than a single number between 0 and 127, and having each transaction type pick a letter rapidly runs into problems with keeping track of which letters we have used and which we haven’t (much easier when we just start at 1 and count up). Also, we would need to map all of the transactions to printable ASCII characters, which wouldn’t align exactly with the actual ASCII code values (e.g., 0 is non-printing, so we would need 0 to mean something else). Once we have a mapping, we might as well map 0 to “Legacy Transaction” which is human readable.

New transaction types need not replace the old in sequence, but rather we could support up to 127 types which will be defined the same way we allocate opcodes. For example, I may want to define a quantum resistant transaction ‘q’ or a byte-boundary packed transaction ‘p’ which would be smaller than RLP. I already wrote up a use-case for ‘d’ ditto transactions, it doesn’t really make sense to call this transaction #1. Using #0 as the archetypal transaction doesn’t bother me - but perhaps enforcing a sequence here would breed confusion as types of transaction expands.

This follows the “char enum” pattern that you see in PostgreSQL.