Skip to content

Frame transactions

A standard Ethereum transaction is a fixed shape: one call, to one address, authorised by one ECDSA signature over the transaction hash. The signature is not a field you can swap — the sender’s identity is derived from it.

EIP-8141 — a draft proposal co-authored by Vitalik Buterin to make Ethereum signature-agnostic — replaces that shape with a more general one. A frame transaction, EIP-2718 type 0x06, carries:

  • an ordered list of frames to execute,
  • a list of signature entries that the protocol may or may not interpret,
  • and the sender, named explicitly rather than recovered.

That last point is the hinge. Once the sender is a field rather than a derivation, the signature no longer has to be something the protocol understands. It can be a lattice signature, a hash-based signature, a threshold signature, or a scheme that does not exist yet — as long as the sender’s own code knows how to check it.

A frame is a single call with its own target, value, calldata, and gas limit. Frames execute in order, and each one has a mode that determines what it is allowed to do:

Mode Name Meaning
0x00 DEFAULT An ordinary call.
0x01 VERIFY Static-called — it cannot modify state. To let the transaction proceed it must explicitly APPROVE.
0x02 SENDER Runs with CALLER == tx.sender. Only executes after execution has been approved.

The split between VERIFY and SENDER is the important one. A VERIFY frame is where authorisation happens: it runs read-only, checks whatever it wants to check, and calls the APPROVE opcode if satisfied. A SENDER frame is where the actual work happens, and it only runs if a VERIFY frame approved it.

Each frame also carries flags that bound what its approval can cover — bit 0 for approving payment, bit 1 for approving execution, bit 2 for atomic batching. A frame that is not permitted to approve execution cannot authorise the transaction no matter what it does.

A typical post-quantum spend is two frames:

frames:
[0] VERIFY target: (self) flags: 3 gas: 30,000
→ checks the ML-DSA signature, APPROVEs payment and execution
[1] SENDER target: recipient value: 1 ETH gas: 50,000
→ performs the transfer, with CALLER == the account

The second list is where the witness lives. Each entry is [scheme, signer, msg, signature], and scheme decides who checks it:

Scheme Name Who verifies
0x00 ARBITRARY Nobody, at protocol level. The bytes are simply made available to frame code.
0x01 SECP256K1 The protocol, before execution.
0x02 P256 The protocol, before execution.

The crucial insight is that scheme is a verification mode, not an algorithm label. There is no ML-DSA scheme number, and there does not need to be. A post-quantum signature rides as an ARBITRARY entry: the protocol treats it as opaque bytes, and the account’s VERIFY frame copies those bytes out with the SIGPARAM opcode, appends its own public key, and calls the ML-DSA verifier precompile.

This is what makes the design crypto-agile. Supporting a new signature scheme requires a new precompile and a new account type — not a new scheme number, not a change to what an address means, and not a hard fork of the transaction format.

The circularity problem, and the elision rule

Section titled “The circularity problem, and the elision rule”

There is an obvious problem with putting a signature inside a transaction that the signature signs. The witness signs the transaction hash; the witness is part of the transaction; so the hash depends on the witness, which depends on the hash.

EIP-8141 breaks this with an elision rule. The signature hash is computed over the transaction with one modification:

every signature entry whose msg is empty has its signature field replaced by the empty string.

So the workflow is:

  1. Build the transaction with an empty witness.
  2. Compute the sig-hash over that.
  3. Sign the 32-byte sig-hash with your ML-DSA key.
  4. Attach the signature.

Step 4 does not change the sig-hash, because the elision rule removed those bytes from it in the first place.

This has a useful consequence: because the witness bytes are outside the hash, a relayer that corrupts them can only make the transaction invalid. It can never redirect it, change its recipient, or alter its value. The sig-hash still commits to the chain ID, the nonce, every frame including its calldata, the fee caps, the gas limit, and the shape of the signature list.

Entries with a non-empty msg work the other way round: they sign msg, not the sig-hash, so they are hashed as-is and are never elided.

Two further fields are worth calling out.

sender is a plain 20-byte address in the payload. There is no to field at the transaction level — each frame has its own target, and an empty target means the sender itself.

gas_limit at the transaction level is signed, which is a Frost-specific divergence from the upstream draft. It is covered by the sig-hash, validity requires it to be at least the transaction’s computed total gas, the payer is charged it up front, and unused gas is refunded. Signing it bounds what a relayer can do with the padding room that elided witnesses would otherwise create.

Frame transactions are not free relative to ordinary ones. The intrinsic cost is 15,000 gas plus 475 per frame, plus calldata gas on everything including the signature bytes, plus each frame’s own gas. Most of the cost of a post-quantum spend is calldata gas on a multi-kilobyte witness, not the signature verification itself.

The exact accounting is in the transaction type reference.