PrivacyArchitecture

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:

  1. Alice publishes a stealth meta-address once
  2. Senders derive unique one-time addresses per payment
  3. 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 * G

No 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.

On this page