Viewing Keys
Per-transaction viewing key derivation — how to share selective read access with auditors without compromising spending ability.
Design Status
The viewing key derivation scheme is in active development. The per-note key hierarchy for selective disclosure is stable in principle; cryptographic details may evolve.
Key Hierarchy
Wallet Signature
│
▼
┌─────────────────────────────────────┐
│ Master Viewing Key (MVK) │
│ MVK_priv: scalar from signature │
│ MVK_pub: MVK_priv * G │
└─────────────────────────────────────┘
│
│ derives using R.x as nonce
▼
┌─────────────────────────────────────┐
│ Per-Note Viewing Keys │
│ EVK: encryption key (sender uses) │
│ DVK: decryption key (recipient) │
└─────────────────────────────────────┘Why Per-Note Keys?
Sharing your master viewing key exposes your entire history. Per-note keys let you:
- Share specific notes for auditing
- Keep all other notes private
- Future notes remain protected even after sharing
The R.x Nonce Approach
Each note has an ephemeral public key R generated by the sender. The note-specific keys are derived using R.x as a nonce:
// Sender side
const r = randomScalar()
const R = r * G
// Derive per-note keys using R.x
const offset = Poseidon(MVK_pub.x, MVK_pub.y, R.x)
// EVK = MVK_pub + offset * G (sender encrypts with this)
// DVK = MVK_priv + offset (recipient decrypts with this)Why R.x and not the leaf index? The sender doesn't know the leaf index when creating the note — it's determined when the transaction mines. R.x is known at creation time.
Encryption Flow
Sender side:
const r = randomScalar(), R = r * G
// 1. Derive EVK using R.x
const evk = deriveEVK(recipientMVKPub, R)
// 2. ECDH shared secret
const shared = r * evk
// 3. Encrypt note with AES-GCM
const ciphertext = encrypt(shared, noteData)
// 4. Include R for the recipient
return { ciphertext, R }Recipient side:
const R = note.ephemeralPubKey
// 1. Derive DVK using same R.x
const dvk = deriveDVK(mvkPriv, mvkPub, R)
// 2. ECDH shared secret
const shared = dvk * R
// 3. Decrypt
const noteData = decrypt(shared, ciphertext)Audit Export
Share read access to specific notes without exposing your DVK scalar:
const auditExport = await exportViewingKeysForAudit(keys, address, [
{ leafIndex: 42, ephemeralPubkeyX: note1.R.x },
{ leafIndex: 57, ephemeralPubkeyX: note2.R.x },
])
// Export contains ECDH shared secrets (points), not DVK scalars
// viewingKey: [sharedSecret.x, sharedSecret.y]Security
Audit exports contain the ECDH shared secret point, not the DVK scalar. Recovering the DVK from the shared secret requires solving the Elliptic Curve Discrete Logarithm Problem (~128-bit security). Auditors can decrypt specified notes but cannot derive keys for other notes.
Search Tag Optimization
To avoid decrypting every note, a 64-bit search tag filters candidates:
const tag = Poseidon(sharedPoint.x, 0n) & ((1n << 64n) - 1n)Recipients compute the expected tag, skip non-matches, and only fully decrypt potential matches. This filters ~99.999% of irrelevant notes.