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
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 addressWhat 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
// 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
- Umbra docs
- State compression on Solana — same Merkle tree primitive underpins shielded balances
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.