> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.umbraprivacy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Stealth Pool Notes

> Stealth Pool internals: Indexed Merkle Tree of note commitments, nullifier treap for double-spend prevention, X25519 ciphertext discovery, and Groth16 ZK proof verification on burn.

## What is a Stealth Pool Note?

A **Stealth Pool Note** is a [UTXO](https://bitcoin.org/bitcoin.pdf)-style commitment leaf in the mixer's Indexed Merkle Tree. The SDK surfaces these commitments as "notes", and the act of consuming one is a "burn".

A note encodes:

* **Amount** — how many tokens are locked.
* **Destination address** — who is authorised to burn.
* **Secret randomness** — private entropy known only to the writer (and, for receiver-burnable notes, the recipient).

Only the **[Poseidon](https://eprint.iacr.org/2019/458.pdf) hash** of these values (the *commitment*) is stored on-chain. The inputs remain private.

## The Stealth Pool: How It Works

The Stealth Pool is a shared **[Indexed Merkle Tree](https://link.springer.com/chapter/10.1007/3-540-48184-2_32)** stored on-chain. Each leaf in the tree is a note commitment.

<Steps>
  <Step title="Write a note">
    You call one of the four `…StealthPoolNote…Creator` factory functions (see below). The SDK computes a commitment from `(amount, destination, randomness)` and inserts it as a new leaf into the tree. Your tokens are locked in the pool custody account.

    At this point, anyone can see that *a note was written* and *the tree grew by one leaf* — but cannot see the amount, destination, or any other detail.
  </Step>

  <Step title="Build the anonymity set">
    As more users write notes into the same tree, your commitment becomes one of many. The larger the set, the harder it is to link the write to the eventual burn. Trees hold up to **1,048,576 leaves** (depth-20 Indexed Merkle Tree).
  </Step>

  <Step title="Scan">
    The SDK queries the indexer for note ciphertexts, attempts to decrypt each with your viewing keys, and groups successful decrypts by `(burn kind, source)`. The scanner is **zero-arg** — it walks every active tree and tracks its own cursor in `client.utxoDataStore`. See [Scanning](/sdk/mixer/fetching-utxos).
  </Step>

  <Step title="Burn">
    You present a **[zero-knowledge proof](https://dl.acm.org/doi/10.1145/22145.22178)** that proves:

    * You know the secret inputs behind a commitment that exists in the tree.
    * You haven't burnt it before (nullifier is unspent).

    Without revealing *which* commitment it is. The on-chain program verifies the proof, burns the nullifier, and releases the tokens to the destination ETA or ATA.
  </Step>
</Steps>

## The Four Note Variants

A note is parameterised on two axes — **unlocker** (who can burn it) × **source** (where the funds came from):

* **Self-burnable from ATA** — `getATAIntoSelfBurnableStealthPoolNoteCreatorFunction` — sender keeps the unlocker; funded from the writer's ATA. Single tx, no MPC.
* **Self-burnable from ETA** — `getETAIntoSelfBurnableStealthPoolNoteCreatorFunction` — sender keeps the unlocker; funded from the writer's ETA. Two-tx MPC pipeline.
* **Receiver-burnable from ATA** — `getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction` — unlocker encrypted against the recipient's `userCommitment`; funded from the writer's ATA.
* **Receiver-burnable from ETA** — `getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction` — same recipient model, funded from the writer's ETA. Two-tx MPC pipeline.

The "self-burnable" pattern is useful when you want to move funds through the pool yourself. The "receiver-burnable" pattern lets you send tokens anonymously — you write for a recipient, and they burn the note without you having direct access.

## Registration prerequisites differ by variant

This is one of the most common integration footguns.

* **Self-burnable creates** encrypt the unlocker against the **sender's** master-seed-derived key. Sender needs only `isUserAccountX25519KeyRegistered`. Recipient needs nothing.
* **Receiver-burnable creates** encrypt against the **recipient's** `userCommitment`. The recipient **must** have completed **all three** registration sub-steps on-chain (account init, X25519 key, user commitment). Otherwise the create simulation fails before any tx is broadcast.

**Always pre-check the recipient before a receiver-burnable create.** Run `getUserAccountQuerierFunction` on the recipient address; if any flag is missing, either abort with a clear "ask the recipient to register on Umbra" error, **or** fall back to a self-burnable create (the sender stays the unlocker; recipient needs zero on-chain state — useful for one-shot transfers).

```typescript theme={null}
import { getUserAccountQuerierFunction } from "@umbra-privacy/sdk/query";

const query = getUserAccountQuerierFunction({ client });
const r = await query(recipient);

const ready = r.state === "exists"
  && r.data?.isInitialised
  && r.data?.isUserAccountX25519KeyRegistered
  && r.data?.isUserCommitmentRegistered;

if (!ready) {
  // Fall back to a self-burnable note, or ask the recipient to register.
}
```

## Nullifiers: Preventing Double-Spends

Each note has a corresponding **[nullifier](https://eprint.iacr.org/2014/349.pdf)** — a deterministic hash derived from its private inputs. When a note is burnt, its nullifier is stored in an on-chain **[treap](https://dl.acm.org/doi/10.1145/324133.324247)** (a self-balancing sorted tree).

Before allowing a burn, the on-chain program checks that:

* The nullifier has not been seen before.
* The ZK proof is valid for a commitment in the current Merkle tree.

This prevents any note from being burnt twice, even if the burn transaction is replayed. The relayer reports `NullifierAlreadyBurnt` in the failure reason if you re-submit a burn that has already landed — the SDK's burner factory treats this as success and surfaces a clean "Note already burnt" string.

## Ciphertext Discovery

After a note is written, an **X25519 + AES-GCM ciphertext** is published on-chain (and indexed off-chain). For self-burnable notes the ciphertext is addressed to the writer; for receiver-burnable notes it is addressed to the recipient.

The ciphertext payload contains:

* Amount (8 bytes).
* Destination address (32 bytes).
* Generation index (16 bytes).
* Domain separator identifying the note variant (12 bytes).

The Umbra indexer stores all ciphertexts and serves them for efficient querying. Your X25519 private key is used locally by the scanner to try decrypting each one — successful decryptions are your burnable notes.

<Note>
  Your private key never leaves your device. Decryption happens in the SDK using locally derived X25519 keys.
</Note>

## Anonymity Set Size

The privacy guarantee of the pool depends on how many other notes exist in the same tree at the time you burn. A tree with only one leaf offers no privacy — it's obvious which commitment is being burnt.

In practice:

* Wait for more users to write notes before burning.
* Burning into a different address from the write address increases privacy.
* Combining a burn into an ETA (rather than an ATA) hides the destination amount further.

## Trees Fill Up

Each Merkle tree has a maximum of 1,048,576 leaves. When a tree is full, the on-chain write service starts a new tree at the next sequential index. Notes from different trees have separate anonymity sets.

The scanner walks every active tree automatically — you do not need to specify a tree index. (The old `(treeIndex, start, end)` arguments are gone; cursor state lives in `client.utxoDataStore` and is per-tree under the hood.)
