All articles
solanaumbraprivacyzk

Umbra: a privacy layer for Solana — shielded balances and private transfers

Umbra brings shielded balances and private transfers to Solana via ZK proofs. The shape — note commitments, nullifiers, shielded pool, consumer flow.

Solana's default is full transparency — every transfer, balance, and instruction is public forever. For most use cases that's fine; for payroll, treasury, OTC settlement, or personal finance, it's a problem. Umbra is a privacy layer that brings shielded balances and private transfers to Solana using zero-knowledge proofs — same pattern as Tornado Cash on Ethereum, but engineered for Solana's account model and execution environment.

The shielded-pool model

Three primitives underneath the UX:

  • Note commitments. When you deposit, the program records a Pedersen commitment hash of a note (amount + owner + nonce). The commitment goes into an append-only on-chain Merkle tree. The note itself is yours; nobody else sees it.
  • Nullifiers. When you spend a note, you reveal its nullifier (a derived hash that's deterministic from the note's secret but doesn't reveal which note it came from). The on-chain program tracks spent nullifiers in a set; any nullifier seen twice means a double-spend attempt (rejected).
  • ZK proofs. Every shielded transfer attaches a Groth16 proof saying: "I know a valid note whose commitment is in the tree, I'm revealing its nullifier correctly, and I'm creating new notes summing to ≤ the spent amount." The proof reveals nothing else.

The flow

text
Transparent → Shielded (deposit)
  1. User picks an amount, generates a new note (random nonce + amount)
  2. Computes commitment = hash(amount || owner_view_key || nonce)
  3. Transfers the public USDC to the Umbra vault
  4. Umbra program inserts the commitment into the Merkle tree
  5. User now "owns" a note for the deposited amount

Shielded → Shielded (private transfer)
  1. Sender selects one or more existing notes to spend
  2. Generates new notes for recipient(s) and change-to-self
  3. Proves (in ZK): spent notes exist in tree + correctly nullified +
                     new notes sum to ≤ spent amount
  4. Submits to Umbra program: nullifiers + new commitments + proof
  5. Program verifies proof, adds nullifiers to spent set,
     adds new commitments to the tree

Shielded → Transparent (withdraw)
  1. User spends a shielded note via proof
  2. Specifies a transparent recipient address + amount
  3. Umbra program emits the transparent USDC to that address

What stays hidden

  • Note amounts — never on-chain in cleartext
  • Owner pubkeys for shielded balances — only the owner's view key can recognise notes destined for them
  • Transfer graph — every shielded transfer dissolves into the anonymity set of all unspent notes in the tree

What stays visible: the existence of a deposit (linking the depositor's wallet to some note in the tree), the timing of each shielded tx, and the eventual withdraw (linking some tree note to a transparent recipient). The privacy guarantee is "you can't link a specific deposit to a specific withdraw" as long as the anonymity set is large.

The compliance side

Pure-privacy systems often run into compliance walls. Modern privacy-layer designs ship optional view keys — a separate key derivable from the owner key that lets a third party (auditor, employer, regulator) decrypt the owner's notes without being able to spend them.

This lets a corporate user enable a private payroll while still producing auditable books for their accountant, or a regulated DeFi protocol offer privacy while keeping the ability to comply with subpoenas for specific accounts.

Why this is tractable on Solana

  • Sub-cent fees — ZK proof generation is expensive (client-side), but the on-chain verification cost (a few hundred thousand CUs) is affordable.
  • State compression — Solana's native Merkle-tree primitive (used for cNFTs) is a natural fit for the note commitment tree.
  • ed25519 + Groth16 syscalls — the runtime ships precompiled signature and pairing verification opcodes, which keep proof verification CU costs predictable.

Consumer integration

typescript
// Conceptual client-side flow (real APIs differ per implementation)
import { UmbraClient } from "@umbra-cash/sdk"

const client = new UmbraClient({ rpcUrl: "https://...", wallet })

// Deposit 100 USDC into a shielded note
const note = await client.deposit({
  asset: "USDC",
  amount: 100_000_000n,
})

// Send half of it privately to another shielded address
await client.transfer({
  from: [note],
  to: [
    { recipient: "<shielded-addr>", amount: 50_000_000n },
    { recipient: client.selfAddress, amount: 50_000_000n },  // change note
  ],
})

// Withdraw the change back to a transparent address
const remaining = await client.findUnspentNotes("USDC")
await client.withdraw({
  notes: remaining,
  transparentRecipient: payoutWallet.publicKey,
})

The honest read

Privacy on a public chain is a hard problem. Umbra-style designs give you strong cryptographic privacy for the contents of transactions, but they don't hide the fact that you're using a privacy layer at all — which can itself be a signal. The strongest privacy comes from a large anonymity set, which requires meaningful adoption, which is a chicken- and-egg problem every privacy protocol faces.

For payroll, B2B settlement, and personal financial privacy, the protocol-level guarantees are real and useful today. For end-to-end transaction privacy across the chain, the practical privacy floor is the anonymity set, not the cryptography.

References

Privacy layers are the missing default on Solana — every other primitive is open by design. Umbra and similar shielded-pool designs are how that finally changes.

Umbra: a privacy layer for Solana — shielded balances and private transfers | devrels.xyz