Stealth Addresses
Unlinkable payment addresses
Design in Progress
The stealth address scheme is under review as we finalize the overall architecture. While the core concepts (unlinkable payments, key derivation from wallet signature) are stable, specific implementation details may change as we integrate with the note-centric compliance model.
The Problem
If Alice publishes one address:
- All payments to Alice are linkable
- Her incoming transactions are visible
- Her balance is exposed
The Solution
With stealth addresses:
- Alice publishes a stealth meta-address once
- Senders derive unique one-time addresses per payment
- Only Alice can link payments to herself
Key Derivation
Keys derive from a wallet signature:
const signature = await wallet.signMessage("UPP Stealth Key Derivation v1")
const seed = keccak256(signature)
// Spending key - authorizes spending
const spendingSecret = keccak256(seed + "spending:v1") % SUBGROUP_ORDER
const spendingPub = spendingSecret * G
// Viewing key - decrypts notes
const viewingSecret = keccak256(seed + "viewing:v1") % SUBGROUP_ORDER
const viewingPub = viewingSecret * GNo extra seed phrases. Keys derive deterministically from your wallet.
Stealth Meta-Address
Encoded in bech32m format with 0zk prefix:
0zk1sepolia1q2w3e4r5t6y7u8i9o0p...Contains:
- Spending public key
- Viewing public key
- Chain ID
- Version
Share this publicly. Senders use it to create unlinkable payments.
Sending a Payment
// 1. Generate ephemeral keypair (one-time)
const r = randomScalar()
const R = r * G
// 2. ECDH shared secret with recipient's viewing key
const shared = r * recipientViewingPub
// 3. Encrypt note
const ciphertext = encrypt(shared, noteData)
// 4. Include R in output
return { commitment, ciphertext, R }Receiving Payments
// Scan all note events
for (const event of noteEvents) {
// Extract R from encrypted data
const R = decodePoint(event.encryptedData)
// Compute shared secret
const shared = viewingSecret * R
// Try to decrypt
try {
const note = decrypt(shared, event.ciphertext)
if (verifyCommitment(note, event.commitment)) {
myNotes.push(note)
}
} catch {
// Not for us, continue
}
}Security Properties
Unlinkability: Each payment uses fresh ephemeral key. Observers can't link payments to same recipient.
Forward secrecy: Compromising one ephemeral key reveals one payment. Other payments remain private.
View vs Spend: Viewing key decrypts notes. Spending key spends them. Share viewing key for audit without risking funds.
Why BabyJubJub?
Ethereum uses secp256k1, but it's expensive in ZK circuits (~50k constraints per multiplication).
BabyJubJub is ZK-friendly (~1k constraints). Practical for real-world proofs.