Skip to content

Transaction type 0x06

The byte-level authority for building frame transactions. For what they are and why, see Frame transactions.

Signed transactions travel as 0x06 ‖ rlp(payload) via eth_sendRawTransaction. The acknowledgement hash is the keccak-256 of the raw bytes.

payload = [chain_id, nonce, sender, gas_limit, frames, signatures,
tip_cap, fee_cap, blob_fee_cap, blob_hashes]
Frame = [mode, flags, target, gas_limit, value, data]
TxSig = [scheme, signer, msg, signature]
Field Type Notes
chain_id uint 8141
nonce uint The sender account’s nonce. Read it from the chain.
sender 20 bytes The account. Explicit, not recovered.
gas_limit uint Transaction-level, signed. Frost-local field.
frames list 1–64 frames, executed in order.
signatures list 0–128 entries.
tip_cap uint Priority fee. Set to 0 — the chain is fee-blind.
fee_cap uint The bid for inclusion.
blob_fee_cap uint EIP-4844.
blob_hashes list EIP-4844 versioned hashes.

There is no to field. Each frame carries its own target; an empty target means tx.sender.

Field Values
mode 0x00 DEFAULT · 0x01 VERIFY · 0x02 SENDER
flags bit 0 may approve payment · bit 1 may approve execution · bit 2 atomic batch
target 20 bytes, or empty for tx.sender
gas_limit uint, this frame’s budget
value uint, nonzero only in SENDER frames
data bytes, this frame’s calldata

A VERIFY frame is static-called — it cannot modify state — and must call APPROVE for the transaction to proceed. A SENDER frame runs with CALLER == tx.sender, and only after execution approval.

Field Values
scheme 0x00 ARBITRARY · 0x01 SECP256K1 · 0x02 P256
signer 20 bytes; must be absent for ARBITRARY
msg empty, or exactly 32 bytes (and a 32-byte msg must not be all-zero)
signature bytes

scheme is a verification mode, not an algorithm label. ARBITRARY means the protocol does not verify the entry at all; the bytes are made available to frame code via SIGPARAM. This is how a post-quantum signature rides in a transaction the protocol does not understand.

Protocol-verified schemes have fixed sizes: secp256k1 signatures are exactly 65 bytes, P256 exactly 128.

sig_hash = keccak256(0x06 ‖ rlp(payload'))

where payload' equals payload except that every signature entry whose msg is empty has its signature field replaced by the empty string.

Build with an empty witness → hash → sign → attach. Attaching does not change the hash.

Entries with a non-empty msg sign msg rather than the sig-hash, so they are hashed as-is and never elided.

The sig-hash commits to: chain ID, nonce, sender, the signed gas_limit, every frame including its calldata, the fee caps, and the shape of the signature list (count, schemes, and msgs). It does not commit to elided witness bytes — which is why a relayer that corrupts them can only invalidate a transaction, never redirect it.

Consensus rejects — in the pool and in a block — any frame transaction that violates these:

  • 1–64 frames; at most 128 signature entries.
  • mode ≤ 0x02; flags < 8.
  • Nonzero value only in SENDER frames.
  • The atomic-batch flag (bit 2) is invalid on the last frame.
  • Every entry’s msg is empty or exactly 32 bytes, and a 32-byte msg must not be all-zero.
  • ARBITRARY entries must leave signer absent.
  • An ARBITRARY entry with a non-empty msg caps signature at 4,096 bytes — those bytes are permanent.
  • Elidable (empty-msg) witnesses are capped at 16,384 bytes per entry and 16,384 bytes per transaction.
  • secp256k1 signatures are exactly 65 bytes; P256 exactly 128.
  • Expiry-verifier frames (a VERIFY frame resolving to the canonical verifier at 0x…8141): at most one per transaction, flags 0, zero value, and data exactly 8 bytes — the big-endian expiry deadline.
15,000 intrinsic
+ 475 × frame count
+ calldata gas EIP-7623, signature bytes included
+ per-signature verification 2,800 secp256k1 · 6,700 P256 · 0 ARBITRARY
+ permanent-witness surcharge {FRAME_LIMITS.permanentSigByteGas} per byte, non-elidable witnesses only
+ Σ frame gas_limit

Total gas is not a field. It is derived, and the signed gas_limit must be at least equal to it.

The permanent-witness surcharge exists because ARBITRARY entries with a non-empty msg are never elided from the canonical encoding, so witness segregation can never prune them. They live in permanent history and are priced above the elidable-byte floor.

frames:
[0] VERIFY target: ∅ (self) flags: 3 gas: 30,000 data: ∅
[1] SENDER target: recipient value: V gas: 50,000 data: D
signatures:
[0] {ARBITRARY, msg: ∅, signature: ML-DSA-65 over sig_hash} {MLDSA_SIZES.mldsa65.signature.toLocaleString('en-US')} B
frames:
[0] VERIFY target: ∅ (self) flags: 3 gas: 30,000 data: 0x01
[1] SENDER target: ∅ (self) gas: 520,000
data: newActivePk ({MLDSA_SIZES.mldsa65.publicKey.toLocaleString('en-US')} B) ‖ keccak256(newBackupPk) (32 B)
signatures:
[0] {ARBITRARY, msg: ∅, signature: ML-DSA-65 by the BACKUP key} {MLDSA_SIZES.mldsa65.signature.toLocaleString('en-US')} B
[1] {ARBITRARY, msg: ∅, signature: the backup public key blob} {MLDSA_SIZES.mldsa65.publicKey.toLocaleString('en-US')} B

The second entry is a public key, not a signature. It is elided from the sig-hash but authenticated against state — the account hashes it and compares against its stored backup commitment.

Frame transaction receipts carry an overall status, a per-frame receipt array, and a payer field naming the account charged. A VERIFY frame that did not approve and a SENDER frame that reverted are distinguishable.

  • Validate against vectors, not prose. The reference implementation ships pinned test vectors covering every encoding on this page.
  • Do not re-hash a stripped transaction. Below the pruning horizon, eth_getTransactionByHash returns elided entries empty. Those bytes decode but do not hash to the canonical txid. Reassemble from the sidecar, or use a proof.
  • Nonces are contract nonces. A rotatable account is at 3 after its first send, a fixed-key v1 account at 2; spends add 1 and rotations add 2. Always read eth_getTransactionCount.
  • Submit to the ingress. Batched JSON-RPC is rejected.
  • Frame opcodes — the introspection surface frame code uses.
  • Account contracts — what is on the other end.
  • SDKs — implementations that already do all of this correctly.