Skip to main content

The Most Powerful Shielded Pool on Solana

Umbra’s Stealth Pool is the most powerful shielded pool on Solana today — and it is built to fit into any flow. Tokens can enter from a public ATA or an EncryptedTokenAccount, exit to a fresh address or stay private inside another ETA, and be unlocked by you or by any registered recipient. Every combination is a first-class path.
The pool requires registration with anonymous: true on the recipient side for receiver-burnable notes. Self-burnable notes require no recipient registration — the sender is the unlocker.

Note structure

The pool operates on Stealth Pool Notes — commitments inserted into an on-chain Indexed Merkle Tree that represent locked tokens. Every note encodes three distinct roles:
  • Sender — the address that funded the note, locked the tokens into the pool, and fixed the recipient at creation time.
  • Unlocker — the address authorised to burn the note’s nullifier on-chain and release the tokens, choosing whether they exit to an ATA or an ETA.
  • Recipient — the final destination address, set by the sender and not modifiable by the unlocker.
The sender controls who receives. The unlocker controls when and how the tokens exit. Separating these two roles is what makes deep mixing possible — the sender’s involvement ends at note-write time.

Note variants

Self-burnable

You are both the sender and the unlocker. Burn the note on your own schedule and choose whether tokens exit to an ATA or stay in an ETA. Recipient needs zero on-chain state.

Receiver-burnable

The recipient is the unlocker. They burn the note themselves and decide the exit. Recipient must be fully registered (all three sub-step flags) — the unlocker is encrypted against their userCommitment.
Receiver-burnable notes produce stronger anonymity sets. Because the sender’s actions and the exit are fully decoupled, timing correlation between write and burn becomes significantly harder.

Source options

Each variant can be funded from two sources:
  • From an ATA — tokens are transferred directly from your public ATA. Single tx, no MPC.
  • From an ETA — tokens are drawn from your existing EncryptedTokenAccount before entering the pool. Two-tx pipeline (proof account → note write), MPC under the hood.
This gives four creator factories — all under @umbra-privacy/sdk/deposit:
  • getATAIntoSelfBurnableStealthPoolNoteCreatorFunction — ATA source, you unlock.
  • getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction — ATA source, recipient unlocks.
  • getETAIntoSelfBurnableStealthPoolNoteCreatorFunction — ETA source (MPC), you unlock.
  • getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction — ETA source (MPC), recipient unlocks.
See Writing notes for full API details and the recipient pre-check rubric.

Burn variants (V18)

Three burn factories are shipped, under @umbra-privacy/sdk/burn:
  • getReceiverBurnableStealthPoolNoteIntoETABurnerFunction — receiver-burnable → ETA. Natively batches: groups notes by destinationAddress, chunks ≤5 per proof.
  • getSelfBurnableStealthPoolNoteIntoETABurnerFunction — self-burnable → ETA. MAX_NOTES_PER_PROOF = 1; SDK loops internally.
  • getSelfBurnableStealthPoolNoteIntoATABurnerFunction — self-burnable → ATA. MAX_NOTES_PER_PROOF = 1.
(Receiver-burnable → ATA is supported on-chain but is not yet shipped in the SDK — open issue.)

The Stealth Pool flow

1

Write a note

Choose a creator factory, supply the recipient pre-check / fallback (for receiver-burnable creates), and submit. The SDK locks tokens in the pool, inserts a commitment into the Merkle tree, and publishes the X25519-AES ciphertext on-chain so the unlocker can discover it.See Writing notes.
2

Wait for the anonymity set to grow

The more other users write notes into the same tree, the stronger your privacy guarantee. There is no enforced waiting period — this is a trade-off you manage in your application.
3

Scan for burnable notes

Run getBurnableStealthPoolNoteScannerFunction({ client })(). The zero-arg scanner walks every active tree, decrypts every ciphertext addressable by your viewing keys, persists progress in client.utxoDataStore, and returns the notes grouped by (kind, source).See Scanning.
4

Burn

Pass the notes to the matching burner factory. The burner fetches a per-batch Merkle proof, generates a Groth16 ZK proof, submits to the relayer, and polls until the on-chain burn lands. The nullifier is burned to prevent double-spending; tokens are released to the chosen destination.See Burning.

Infrastructure requirements

  • Indexer — required for note discovery and Merkle proof fetching. Pass indexerApiEndpoint to getUmbraClient.
  • Relayer — required for burning. Pass relayerApiEndpoint to getUmbraClient, then build the burner factory’s relayer dep from getUmbraRelayer({ apiEndpoint }).
  • Store adapters — strongly recommended on the browser. Without utxoDataStore / nullifierStore, every scan() re-scans every active tree from genesis.
import { getUmbraClient } from "@umbra-privacy/sdk";
import {
  createBrowserStorageBackend,
  createShardedUtxoDataStore,
  createShardedNullifierStore,
} from "@umbra-privacy/sdk/store-adapters";

const storageBackend = createBrowserStorageBackend({ dbName: "umbra" });
const utxoDataStore  = createShardedUtxoDataStore({ storageBackend });
const nullifierStore = createShardedNullifierStore({ storageBackend });

const client = await getUmbraClient({
  signer,
  network: "mainnet",
  rpcUrl: "...",
  rpcSubscriptionsUrl: "...",
  indexerApiEndpoint: "https://utxo-indexer.api.umbraprivacy.com",
  relayerApiEndpoint: "https://relayer.api.umbraprivacy.com",
  utxoDataStore,
  nullifierStore,
});

Privacy Considerations

Burn into an ETA by default. When a note is burnt into an ETA, the burned amount is hidden, the destination is not revealed, and no observable exit event is produced on-chain. At burn time, the sender is completely unlinkable — there is no visible amount, no destination address, and no signal that can tie the burn back to any specific note write. ATA burns should be the opt-out, not the default. Use round, pool-common amounts when burning publicly. If a burn must exit to an ATA, the amount becomes visible on-chain. Unusual or fragmented amounts let an observer eliminate most of the tree and narrow in on the matching commitment. Standardise on amounts that are common in the pool (e.g. exactly 100 USDC) to stay indistinguishable within the largest possible set. Burn to a fresh address when going public. Burning into an address with existing on-chain history re-establishes a link. Use an address that has never appeared on-chain before. For a complete breakdown of all source × destination combinations — visible vs hidden state, timing risk, recommended use cases — see Privacy Analysis.