Stealth Addresses
Unlinkable payment addresses — how senders create one-time addresses for each payment so recipients can't be tracked on-chain.
Design Status
The stealth address scheme is under review as we finalize the architecture. Core concepts (unlinkable payments, key derivation from wallet signature) are stable. Implementation details may evolve.
The Problem
If Alice publishes one address:
- All payments to Alice are linkable on-chain
- Her incoming transaction history is visible
- Her balance can be inferred
The Solution
With stealth addresses:
- Alice publishes a stealth meta-address once
- Each sender derives a unique one-time address for their payment
- Only Alice (with her viewing key) can link payments to herself
Key Derivation
All keys derive deterministically from a 32-byte seed — no extra seed phrases needed.
The seed can come from two sources:
Option A: Wallet Signature (Default)
Sign an EIP-712 message with your Ethereum wallet:
const signature = await wallet.signTypedData({ /* EIP-712 UPP Connect */ })
const seed = keccak256(signature)Option B: Passkey (WebAuthn PRF)
Use your device's biometric or PIN — no wallet needed:
import { createPasskeyWithPRF } from '@permissionless-technologies/upp-sdk'
// Register a passkey with PRF extension
const { credentialId, dualKeys } = await createPasskeyWithPRF()
// Or authenticate with an existing passkey
const { seed, dualKeys } = await getPasskeyPRFSecret()The WebAuthn PRF extension extracts a deterministic secret from the passkey. Same passkey + same salt = same keys, always. Supported in Chrome 120+, Safari 18+, and Edge 120+. Third-party password managers (Bitwarden, 1Password) may not support PRF yet — use your device's built-in passkey (Touch ID, Windows Hello) instead.
From Seed to Keys
Both paths produce the same 32-byte seed, which feeds into hash-based key derivation:
// Spending key — authorizes spending notes
const spendingSecret = keccak256(seed + ":spending:v1") % FIELD_PRIME
const ownerHash = Poseidon(spendingSecret) // hash-based ownership, no curve
// Viewing key — decrypts notes (share for auditing without risking funds)
const viewingSecret = keccak256(seed + ":viewing:v1") % FIELD_PRIME
const viewingHash = Poseidon(viewingSecret)An optional password can be mixed in to derive different accounts from the same wallet or passkey.
Stealth Meta-Address
Encoded in bech32m format with 0zk prefix:
0zk1sepolia1q2w3e4r5t6y7u8i9o0p...Contains:
- Owner hash (
Poseidon(spendingSecret)) - Viewing hash (
Poseidon(viewingSecret)) - Chain ID (prevents cross-chain replay)
- Version
Share this publicly. Senders use it to create unlinkable payments.
Sending a Payment
// 1. Generate ephemeral keypair (fresh random each payment)
const r = randomScalar()
const R = r * G
// 2. ECDH shared secret with recipient's viewing key
const shared = r * recipientViewingPub
// 3. Encrypt note with derived secret
const ciphertext = encrypt(shared, noteData)
// 4. Include R in the output — recipient needs it to decrypt
return { commitment, ciphertext, R }Receiving Payments
// Scan all note events on-chain
for (const event of noteEvents) {
const R = decodePoint(event.encryptedData)
// Compute shared secret with your viewing key
const shared = viewingSecret * R
// Try to decrypt — if it fails, note is not yours
try {
const note = decrypt(shared, event.ciphertext)
if (verifyCommitment(note, event.commitment)) {
myNotes.push(note)
}
} catch {
// Not for us — continue scanning
}
}Security Properties
| Property | Description |
|---|---|
| Unlinkability | Each payment uses a fresh ephemeral key — observers can't link payments to the same recipient |
| Forward secrecy | Compromising one ephemeral key reveals only one payment |
| Separate view/spend | Viewing key decrypts notes. Spending key spends them. Share viewing key for auditing without risking funds. |
Hash-Based Ownership
UPP uses hash-based ownership proofs instead of elliptic curve operations. Note ownership is proven by demonstrating knowledge of the preimage: "I know spendingSecret such that Poseidon(spendingSecret) == ownerHash."
This is more efficient in ZK circuits (a single Poseidon hash vs. curve point multiplication) and avoids reliance on the discrete log assumption, which is vulnerable to quantum computers.
For the STARK vault, keys use Keccak-256 over the M31 field instead of Poseidon, providing full post-quantum security. See STARK Vault for details.
Notes and Account Model
UPP uses a UTXO-style note model instead of account balances — encrypted UTXOs that prove ownership without revealing contents.
Viewing Keys & Key Hierarchy
4-tier graduated key disclosure — from per-note viewing keys to full spending authority, with nonce chain completeness for provable audits.