// schema · monero protocol v18 · rct type 6

Anatomy of a CLSAG-signed
RingCT transaction.

Every Monero transaction is a stack of cryptographic tricks designed to answer one impossible question: "How do you prove a payment is valid without revealing who sent it, who received it, or how much?" This schema unpacks the four primitives that make it work — stealth addresses, Pedersen commitments, ring signatures (CLSAG), and key images — and shows how they fit together inside one transaction blob.

edwards25519 ring size 16 bulletproofs+ rcttype 6 since oct 2020 (clsag)
01 · Overview

§1Four primitives, four secrets

A public blockchain leaks sender, receiver, and amount by default. Monero closes each leak with a distinct piece of cryptography, then adds a key image to keep the system sound under those blinders. CLSAG is the ring-signature half of the bundle; RingCT is the umbrella term for the whole construction (rings + commitments + range proofs).

R

STEALTH ADDRESS

Hides the receiver. Each output is sent to a fresh one-time pubkey derived from the recipient's address + a per-tx nonce. The recipient's main address never appears on chain.

C

PEDERSEN COMMITMENT

Hides the amount. Each output value is locked inside C = aG + xH. Nodes verify sum(in) − sum(out) − fee = 0 without ever seeing a.

σ

CLSAG RING SIG

Hides the sender. Real input is mixed with 15 decoy outputs pulled from the chain. Signature proves one of the 16 keys signed, without revealing which.

I

KEY IMAGE

Prevents double-spend. A deterministic tag I = x·Hp(P) derived from the real input's private key. Same input → same tag → network rejects duplicates.

The tx as a whole

INPUTS · vin spent by sender RING · 16 members P0 P1 P2 P3 P4 P* P6 P7 P8 P9 P10 P11 P12 P13 P14 P15 ★ P* = real input · others = decoys key_image I = x·H_p(P*) pseudoOut C_offset (commits to amount) CLSAG linkable ring signature proves: signer knows secret for ONE of 16 + pseudoOut commits to same amount σ = { s[16], c1, D } 17 scalars + 1 aux key image ~ 25% smaller than MLSAG (since oct 2020) signs OUTPUTS · vout created for receivers STEALTH ADDRS P_out = H_s(rK^v)·G + K^s one-time keys recipient scans w/ private view key COMMITMENTS C_out = a·G + x·H amount hidden + Bulletproof+ range proof BALANCE · Σ pseudoOuts = Σ outPk + fee · H verified on the blinded values — no plaintext amounts ever exposed
02 · receiver privacy

§2Stealth addresses · one-time output keys

A Monero address is two public keys: Ks (public spend key) and Kv (public view key). When Alice pays Bob, she does not use his address as the destination on chain. Instead, she derives a fresh one-time public key P from his address plus a random per-tx scalar r. Only Bob can recognize that P belongs to him, because the recognition equation requires his private view key kv.

ALICE sender picks random r DERIVATION · per output R = r · G tx public key, goes in `extra` field P = Hs(r · Kv, t) · G + Ks one-time output key · goes in `vout[t].key` view_tag = H(r · K^v, t)[:1] 1-byte hint added in v15 (2022) to speed up scanning BOB receiver has k^v, k^s BOB SCANS · for each new tx P =? Hs(kv · R, t) · G + Ks → match → output is mine
a

Why a fresh key every time

Reusing an address makes payments trivially linkable. Stealth keys mean the chain never sees Bob's main address — only fresh one-time keys he can claim.

b

View key separation

Bob's private view key kv recognizes incoming outputs. His private spend key ks is needed to actually spend them. Auditors get kv, not ks.

c

Spending later

To spend output P, Bob computes the one-time private key x = Hs(kv·R, t) + ks. From x, the ring signature & key image follow.

03 · amount privacy

§3Pedersen commitments + Bulletproofs+

Amounts are never written in clear. Each output's value a is wrapped in a commitment C = aG + xH, where x is a random blinding scalar and G, H are independent generators on edwards25519. The commitment is binding (you can't change a later) and hiding (nobody learns a from C alone). The genius is in the arithmetic: commitments add linearly, so balance can be verified on the blinded values.

C = aG + xH a = amount (u64 atomic units, 1 XMR = 1012) · x = blinding mask (random scalar) · G, H = independent ed25519 generators
balance: Σ Cin Σ Cout fee·H = 0 Sender chooses blinding masks so that Σxin = Σxout. Equation collapses to the amount check: Σain = Σaout + fee — proved without revealing any a or x.

The negative-amount problem

Nothing in C = aG + xH stops a malicious sender from picking a negative a (i.e. a scalar larger than the curve order's midpoint, wrapping mod-L). A negative output amount would let two real outputs cancel out and conjure XMR from thin air. The fix: every output ships with a range proof showing 0 ≤ a < 264.

α

Borromean (legacy)

Original range proof used bit-decomposition + ring sigs per bit. Worked, but ~6.2 kB per 2-output tx.

β

Bulletproofs (2018)

Logarithmic-size range proofs via inner-product argument. Dropped tx size by ~80%. No trusted setup required.

γ

Bulletproofs+ (2022)

Tighter weighted inner-product. ~5-7% smaller than BP, faster to verify. Current default in RCTTypeBulletproofPlus.

Encrypted amount for the receiver

The recipient needs to know a and x to later spend the output. They are encrypted with a shared secret derived from r·Kv and stored in the ecdhInfo field. Only the holder of the matching private view key can decrypt them.

04 · sender privacy

§4The ring & the key image

For each real input the wallet wants to spend, it picks 15 decoys from the chain's pool of past RingCT outputs (gamma-distributed by age — older outputs are weighted lower). The 16 keys form a ring. CLSAG proves that one of the 16 was signed by its true owner, without telling verifiers which. The decoys' commitments are not stored in the signature (a CLSAG win over MLSAG) — only their stealth addresses are referenced via key_offsets.

Key image · the anti-double-spend tag

The key image I is a deterministic function of the real input's one-time private key. Two spends of the same output would produce the same key image, but two different outputs cannot collide (collision-resistance of Hp). Every node maintains a set of seen key images — any tx whose I is already in the set is rejected.

I = x · Hp(P) x = one-time private spend key of the real input · Hp(P) = hash-to-point of its one-time public key · I is unique per output, public, and unlinkable to P
i

Decoy selection

Wallet samples 15 prior outputs from the chain with a gamma distribution matched to real-world spend timing — recent outputs are more likely, mimicking organic behavior.

ii

Ring assembly

Real key inserted at a random index l. The 16 entries are stored as key_offsets (delta-encoded indices into the global output set) — saves bytes vs absolute indices.

iii

Key image emitted

Wallet computes I = x·Hp(P*) for the real input P* and places it in vin[i].k_image. This is the lifetime ID of that output's spend.

iv

Network rejects dupes

On receive: node checks I against its key-image set. Already present → invalid tx, score peer down. Validated → I permanently added.

05 · signature

§5CLSAG · concise linkable spontaneous anonymous group

CLSAG (deployed October 2020, replacing MLSAG) is a linkable ring signature that proves two facts at once: (1) the signer knows the private key for one of the ring's pubkeys and (2) the signer knows the blinding factor that makes C[l] − C_offset a commitment to zero (i.e. the pseudo-output commits to the same hidden amount as the real input). The "concise" part: by aggregating the two proofs with hashed scalars μP and μC, the signature only stores one ring instead of two.

Inputs to the signer

SymbolTypeMeaning
m32-byte hashMessage digest: H(H(P1) ∥ H(P2) ∥ H(P3)) — covers every tx field except the CLSAGs themselves.
pscalarPrivate spend key of the real input (the one-time key the receiver derived earlier).
P[16]point[16]The ring's stealth addresses. P[l] = p·G at the secret index l.
zscalarBlinding mask of the pseudo-output commitment C_offset.
C[16]point[16]Ring commitments. C[l] = z·G after subtracting C_offset.
C_offsetpointThe pseudoOut: a fresh commitment to the same amount as the real input, with a sender-chosen mask. Lets the balance equation close.

The signature itself

σ = { s[16], c1, D } s = 16 ring responses · c1 = challenge seed (forces the ring loop to close) · D = (1/8)·z·Hp(P[l]) auxiliary key image for the commitment ring · I = key image (stored separately in vin)

Construction in five strokes

1

Aggregate

Hash ring data into domain-separated scalars μP and μC. They fold the two proofs (key & commitment) into a single ring.

2

Commit α

Pick random scalar α. Compute L = αG, R = α·Hp(P[l]). These are the "real" challenge inputs at index l.

3

Loop the ring

Starting at l+1, pick random s[i] and rebuild Li, Ri, hash forward to next challenge ci+1. Wrap around back to l.

4

Close it

Set s[l] = α − cl(p·μP + z·μC). The loop closes only if the signer truly knows p and z at index l.

Verification (any node, any time)

A verifier doesn't know l, α, p, or z. It re-derives μP, μC from public data, then walks the ring from i=0: at each step, recompute Li = s[i]·G + c·μP·P[i] + c·μC·(C[i]−C_offset) and Ri = s[i]·Hp(P[i]) + c·μP·I + 8·c·μC·D, hash to the next challenge. After 16 hops, the loop should land back on c1. If it does, the signature is valid — and the verifier still has no idea which index was real.

06 · serialization

§6The transaction blob, field by field

A serialized Monero tx (RCTTypeBulletproofPlus, current default) is split into a non-prunable core and a prunable proofs section. The prunable part can be discarded by pruned nodes after deep confirmation — they keep the key images and commitments forever, but drop the bulky range proofs and ring signatures.

FieldWhereWhat's in it
Core (always kept)
versiontx prefixTx format version. Currently 2 (post-RingCT).
unlock_timetx prefixBlock height or timestamp until which outputs are locked. Almost always 0.
vin[]tx prefixFor each input: amount (always 0 in RingCT), key_offsets[] (delta-encoded indices of the 16 ring members), k_image (the key image I).
vout[]tx prefixFor each output: dummy amount=0, target.key (stealth address P), view_tag (1 byte, since v15).
extratx prefixTag-length-value blob. Holds the tx public key R, optional additional pub keys (for multi-output to subaddresses), and the encrypted payment ID.
rct_signatures (non-prunable parts)
typerct6 = RCTTypeBulletproofPlus (current). Older types: 1 Full, 2 Simple, 3 BP, 4 BP2, 5 CLSAG.
txnFeerctFee in plaintext (verifiers need it for the balance equation). Paid to whichever miner includes the tx.
ecdhInfo[]rctPer output: encrypted (amount, mask) blob using shared secret H(r·Kv). Only receiver can decrypt.
outPk[]rctThe output commitments C_out = a·G + x·H — these are public.
rct_prunable (can be dropped by pruned nodes)
bpp[]prunableBulletproof+ aggregated range proof. One per tx, proves all output amounts are in [0, 264).
CLSAGs[]prunableOne CLSAG σ = {s, c1, D} per input. ~96 bytes + 32·ring_size for the s vector.
pseudoOuts[]prunableOne C_offset per input. Sums to Σ outPk + fee·H — closing the balance equation.
07 · verification

§7What a node checks before letting it in

When monerod receives a tx (whether from a peer's stem-relay or fluff broadcast — see the P2P schema), it runs the full battery below before admitting it to the local tx_pool. Any single failure = reject, peer offense score down.

1

CLSAG validity

For each input: re-derive μP, μC, walk the ring from c1, confirm the loop closes. Proves some ring member signed.

2

Key image uniqueness

Check I is not in the chain's key-image set, nor in any other pool tx. Also: I must lie in the main subgroup (no torsion attacks).

3

Bulletproof+ validity

Verify the aggregated range proof. Confirms every output amount is in [0, 264) — no negative-amount inflation.

4

Commitment balance

Σ pseudoOuts − Σ outPk − fee·H = 0. The single equation that proves no XMR was created or destroyed.

5

Ring member sanity

Every key_offset resolves to a real, on-chain output of compatible age (10-block lock) and matching version.

6

Weight, fee, version

Tx weight under cap. Fee ≥ dynamic min (depends on network load). Version matches current hard-fork rules.

After all checks pass

The tx is added to tx_pool, indexed by hash, sorted by fee-per-byte. The daemon then relays it via Dandelion++ (stem if in stem mode, fluff otherwise). The included key images are not yet committed to the chain — they only become permanent once the tx lands in a block. Until then a pool tx can be evicted, expired, or replaced by an alternate spend of the same input.