UPP — Universal Private PoolConcepts

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:

  1. Alice publishes a stealth meta-address once
  2. Each sender derives a unique one-time address for their payment
  3. Only Alice (with her viewing key) can link payments to herself

Key Derivation

All keys derive from a single wallet signature — no extra seed phrases:

const signature = await wallet.signMessage("UPP Stealth Key Derivation v1")
const seed = keccak256(signature)

// Spending key — authorizes spending notes
const spendingSecret = keccak256(seed + "spending:v1") % SUBGROUP_ORDER
const spendingPub = spendingSecret * G

// Viewing key — decrypts notes (share for auditing without risking funds)
const viewingSecret = keccak256(seed + "viewing:v1") % SUBGROUP_ORDER
const viewingPub = viewingSecret * G

Stealth Meta-Address

Encoded in bech32m format with 0zk prefix:

0zk1sepolia1q2w3e4r5t6y7u8i9o0p...

Contains:

  • Spending public key
  • Viewing public key
  • 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

PropertyDescription
UnlinkabilityEach payment uses a fresh ephemeral key — observers can't link payments to the same recipient
Forward secrecyCompromising one ephemeral key reveals only one payment
Separate view/spendViewing key decrypts notes. Spending key spends them. Share viewing key for auditing 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 per multiplication). This makes proving note ownership practical in real-world circuits.

Post-Quantum Note

ECDH on BabyJubJub is not post-quantum secure. For the STARK vault, keys are derived via Keccak hash instead of ECDH. See STARK Vault for details.

On this page