Technical › Network

A transaction byte by byte

A full breakdown of a RingCT transaction's hex: every field, every function — on real and random data

In short

A Monero transaction is three consecutive chunks of bytes: the prefix (inputs, outputs, extra), the RingCT base (fee, encrypted amounts, commitments) and the prunable part (Bulletproofs+, CLSAG, pseudo-outputs). Below is a tool that parses any v2 transaction byte by byte, computes the txid, checks the balance and — given a view key — finds "your" outputs and decrypts their amounts. For a random transaction it shows all the secrets, so you can see how every function is applied to actual numbers.

How to read the bytes

Monero's serialization is simple — no headers or field names, just values one after another. Two kinds of building blocks:

  • varint — a variable-length integer: 7 bits per byte, the top bit means "another byte follows". 0x02 = 2, 0x80 0x01 = 128, 0x80 0xb5 0x18 = 400000.
  • key / point / scalar — exactly 32 bytes, no length. Arrays of points whose length is known from context (e.g. CLSAG's s, by ring size) also come without a length.

So the parser must know the format in advance: the RingCT type, read in the middle, decides how to read everything after it.

prefix version · unlock_time inputs: ring (16 offsets) + key image outputs: one-time key P + view tag extra: R, payment ID… RingCT base type = 6 fee (in the clear) amounts (8 B, enc.) outPk = C prunable Bulletproofs+ CLSAG: s×16, c1, D pseudoOuts C̃ can be dropped by a pruned node Keccak Keccak Keccak txid = Keccak(h₁ ‖ h₂ ‖ h₃)

Which functions are applied where

FieldComputed byFormulaMore
extra: RsenderR = r·Gstealth addresses
output: PsenderP = Hs(8·r·A ‖ i)·G + Bstealth addresses
view tagsenderKeccak("view_tag" ‖ 8rA ‖ i)[0]view tags
ecdhInfosenderv ⊕ Keccak("amount" ‖ Hs(8rA ‖ i))[0..8]Pedersen
outPksenderC = y·G + v·H, y = Hs("commitment_mask" ‖ Hs(8rA ‖ i))Pedersen
ringsender's wallet16 global indices, gamma distribution over agedecoys
key imageinput ownerI = x·Hp(P)key image
pseudoOutssenderC̃ = y'·G + v·H, masks chosen so Σ C̃ = Σ C + fee·HRingCT
CLSAGinput ownera ring of challenges over P_i and C_i − C̃ring signatures
Bulletproofs+senderproof that 0 ≤ v < 2⁶⁴ for all outputsrange proofs

What a node does with these bytes

  1. Parses them; any extra or missing byte means rejection.
  2. Checks the key images are new and in the correct subgroup.
  3. Rebuilds the 16 global indices from the offsets and loads those outputs' keys and commitments from its database.
  4. Checks the balance Σ C̃ = Σ outPk + fee·H — the tool below does this too.
  5. Verifies Bulletproofs+ and each input's CLSAG.

What the recipient's wallet does

  1. Takes R from extra and computes the shared secret 8·a·R (one operation per transaction).
  2. For each output: a hash → compare the view tag; no match — stop.
  3. Match — compare Hs(8aR ‖ i)·G + B with the output key.
  4. The output is ours — decrypt the amount, rebuild the mask and check y·G + v·H against outPk.

The tool shows all these steps with concrete numbers.

Checked on mainnet

The parser was checked against monerod's JSON decoding of real type 5 and type 6 transactions and a coinbase transaction: every field and the txid match. The "real tx from mainnet" button loads one of them — compare the txid with any block explorer.

What will an FCMP++ transaction look like?

The FCMP++ transaction format may still change before the hard fork, so there's no byte parser for it here. But from the current implementation the overall structure is clear — and it's surprisingly similar to today's:

prefix version · unlock_time inputs: only key image L (no more ring offsets) outputs: key O (Carrot) + 3-byte view tag extra: ephemeral key, … base new RingCT type fee amounts + C reference block (which tree root) prunable Bulletproofs+ per input: Õ, Ĩ, R, C̃ per input: SA+L one membership proof for all inputs instead of "16 indices + CLSAG" per input — "a rerandomized tuple + SA+L" and one shared membership proof layout per the current implementation; format details may change before the hard fork
In RingCTIn FCMP++Functions
16 offsets per inputa reference to the block whose tree the proof is built againstCurve Tree root over all outputs
key image I = x·Hp(P)the same L = x·Hp(O)double spends caught as before
pseudo-output C̃C̃ = C + r_c·G — part of the rerandomized tuplererandomization
CLSAG per inputSA+L per input (a few points and scalars)generalized Schnorr
—one membership proof for the whole transactionGeneralized Bulletproofs + divisors
1-byte view tag, ECDH on Ed255193-byte view tag, X25519, Janus anchorCarrot

See rerandomization on actual numbers in the tool. Once the format is final, a byte-level breakdown of FCMP++ transactions will be added here too.