PrivacyArchitecture

Viewing Keys

Per-transaction viewing key derivation

Design in Progress

The viewing key derivation scheme is in active development. The per-note key hierarchy enables selective disclosure for auditing, which is essential for compliance. Specific cryptographic details may evolve as we finalize the architecture, but the principle of separating viewing and spending capabilities will remain.

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 key exposes everything. Per-note keys let you:

  • Share specific notes for auditing
  • Keep other notes private
  • Future notes remain protected

The R.x Nonce Approach

Each note has an ephemeral public key R generated by the sender.

// Sender generates R
const r = randomScalar()
const R = r * G

// R.x becomes the nonce for key derivation
const offset = Poseidon(MVK_pub.x, MVK_pub.y, R.x)

// EVK = MVK_pub + offset * G (public key)
// DVK = MVK_priv + offset     (private key)

Why not leaf index? 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:

// 1. Generate ephemeral keypair
const r = randomScalar(), R = r * G

// 2. Derive EVK using R.x
const evk = deriveEVK(recipientMVKPub, R)

// 3. ECDH shared secret
const shared = r * evk

// 4. Encrypt note with AES-GCM
const ciphertext = encrypt(shared, noteData)

// 5. Include R in output for recipient
return { ciphertext, R }

Recipient side:

// 1. Extract R from encrypted note
const R = note.ephemeralPubKey

// 2. Derive DVK using same R.x
const dvk = deriveDVK(mvkPriv, mvkPub, R)

// 3. ECDH shared secret
const shared = dvk * R

// 4. Decrypt
const noteData = decrypt(shared, ciphertext)

Audit Export

Share viewing access for specific notes:

const auditExport = await exportViewingKeysForAudit(keys, address, [
  { leafIndex: 42, ephemeralPubkeyX: note1.R.x },
  { leafIndex: 57, ephemeralPubkeyX: note2.R.x },
])

// Export contains shared secrets (points), not DVK scalars:
// viewingKey: [sharedSecret.x, sharedSecret.y]

Auditors receive the ECDH shared secret directly - no additional computation needed.

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 expected tag, skip non-matches, only decrypt matches. Filters ~99% of notes.

On this page