EIP-2718 Typed transaction envelope

Final · EVM & execution · EIP status Final · created 2020-06-13

Introduces an envelope format that prepends a single byte indicating the type to the front of a transaction, so that new transaction types can be added in the future without colliding with each other.

Why it is needed

Until now, every time Ethereum added a new transaction type, it had to be designed so it would not overlap with existing transaction types. This was necessary because the type needs to be identifiable from the encoded data alone, and no transaction could match two types at once. EIP-155, for example, avoided this problem by bit-packing a new value into an existing field. But as multiple new transaction types were being discussed, such as proposals letting externally owned accounts run code directly in their own context, proposals letting someone other than msg.sender pay gas fees, and layer 1 multisig-related proposals, designing all of them to be mutually compatible became a heavy burden for both EIP authors and clients. This EIP simplifies the problem by introducing an envelope structure with a single type-number byte at the front of a transaction, so that only backward compatibility with existing transactions needs to be preserved once, and from then on only the type numbers need to avoid colliding.

What changes

Today

After EIP-2718

Who is affected

In depth

Transaction structure: TransactionType || TransactionPayload

From FORK_BLOCK_NUMBER onward, the block header's transactions root MUST be the root hash of patriciaTrie(rlp(Index) => Transaction), where Index is this transaction's position within the block and Transaction is either TransactionType || TransactionPayload or LegacyTransaction (|| is the byte concatenation operator). TransactionType is a positive 8-bit number between 0 and 0x7f, and TransactionPayload is an opaque byte array whose interpretation depends on the type, with the concrete format defined by a later EIP. LegacyTransaction is rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s]).

Receipts use the same envelope structure

The receipts root MUST also be the root hash of patriciaTrie(rlp(Index) => Receipt), where Receipt is either TransactionType || ReceiptPayload or LegacyReceipt. ReceiptPayload is an opaque byte array interpreted according to its type, and LegacyReceipt is rlp([status, cumulativeGasUsed, logsBloom, logs]). The receipt's TransactionType MUST match the TransactionType of the transaction with the same Index.

Recommendation to include the type as the first byte of signed data

Signatures for future transaction types SHOULD include the TransactionType as the first byte of the data being signed. This is to prevent a signature for one type from being reused as a signature for another type. The reason it is SHOULD rather than MUST is that there are exceptions: it is not possible for wrapped legacy transactions that must remain compatible with the legacy signing scheme, and some transactions may determine validity by some other means without a signature in the traditional sense. The Security Considerations section STRONGLY recommends this, noting that failing to follow it could make signatures compatible across different transaction types, creating a security vulnerability for users.

Why the type number was capped at 0x7f

0x7f is sufficient for the near future, and the option remains to widen the range later by using the high bit as a continuation bit. Also, legacy transactions always begin with a byte of 0xc0 or higher, so using the 0 to 0x7f range avoids any collision. This lets a client distinguish a new type from legacy by checking whether the first byte falls in [0, 0x7f] versus [0xc0, 0xfe]. 0xff is a value that cannot realistically occur in an RLP-encoded transaction, so it is reserved as a future extension sentinel value.

Why the payload was left as opaque bytes

Defining what follows the type byte as an opaque byte array rather than an RLP list leaves room for other encodings in the future, such as SSZ, LEB128, or fixed-width formats. An algorithm for deciding how to assign type numbers was also discussed, but at the time of writing the standard there was no strong need for one, so it was left out of scope, with a note that a future EIP could define an assignment standard if needed.

ORIGIN and CALLER stay the same regardless of type

There was discussion of defining different return values for the ORIGIN and CALLER opcodes depending on transaction type, but this was not adopted. The goal was to keep the type opaque to contracts, to prevent contracts from behaving differently based on transaction type, and there were also compatibility concerns with existing contracts built on assumptions about ORIGIN and CALLER. Going forward, every transaction type is assumed to have an address that reasonably represents the CALLER of the first EVM frame, and ORIGIN is assumed to always equal that same address. If a type needs to give a contract additional information, a new opcode would be required. This EIP's status is Final, meaning the specification is settled, and it became the common foundation for later typed transactions such as EIP-1559 and EIP-2930.

Original EIP · ← EIPs