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

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.

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.

On this page