Skip to main content
This document explains the core protocol mechanics: how notes work, what commitments and nullifiers are, how the Merkle tree stores state, and what happens step-by-step during deposits, transfers, and withdrawals. Prerequisites: Familiarity with EVM smart contracts, hash functions, and Merkle trees. Basic understanding of what a zero-knowledge proof does (proves a statement without revealing the witness).

The UTXO Model

Privacy Boost uses a UTXO (Unspent Transaction Output) model. Instead of maintaining account balances, the system tracks individual notes, each representing a specific amount of a specific token owned by a specific user. Think of it like cash. You don’t have a “balance of $47,” you have a $20 bill, a $20 bill, a $5 bill, and two $1 bills. To pay someone $15, you hand over the $20 and get $5 back as change. In Privacy Boost:
  • Spending a note publishes a nullifier (marks it as consumed)
  • Receiving creates new notes (adds commitments to the Merkle tree)
  • A transfer consumes input notes and creates output notes, with the ZK circuit enforcing that inputs = outputs + fees

Notes and Commitments

What’s Inside a Note

A note contains three fields: The NPK is derived from the owner’s Master Public Key (MPK) and a per-note random value:
These leading numbers are domain tags that distinguish hash contexts. Note-tree nodes use Poseidon2(left, right) without a tag, while authorization-tree nodes use Poseidon2(5, left, right). The per-note randomness ensures that even if the same user receives the same token and amount twice, the resulting commitments are different.

Commitments

A commitment is the Poseidon2 hash of a note’s contents:
This is what gets stored onchain as a Merkle tree leaf. It reveals nothing about the note’s contents; an observer sees only a 256-bit hash. The 128-bit randomness in NPK makes brute-force inversion computationally infeasible.

Nullifiers

A nullifier is a one-time-use tag that marks a specific note as spent. It prevents double-spending without revealing which note was consumed.

How Nullifiers Work

Each nullifier is derived from the user’s secret nullifying key and the note’s position in the Merkle tree:
The tree number is encoded in the hash domain to prevent cross-tree collisions. An observer sees commitments going in and nullifiers coming out, but can’t link them: the nullifier depends on a secret key and a leaf index, while the commitment depends on NPK, token, and amount. There’s no algebraic relationship between the two. The contract enforces uniqueness: each nullifier can only be recorded once, preventing double-spending.

How Ownership Is Proven

To spend a note, you prove three things inside the ZK circuit:
  1. You know the note’s secrets: The circuit verifies your private fields hash to the correct commitment.
  2. Your account authorized the spend: The circuit proves either a signature from a registered EdDSA key or membership of the operation in an account-owner-approved commitment batch.
  3. The note exists: The circuit verifies a Merkle proof for your commitment.
For internal transfers these checks hide the spent note, account identity, and individual amounts. Withdrawals expose the public destination, token, and payout amount. Forced withdrawals also expose the input commitments, the spender account ID, and the exact authorization leaf used.

Merkle Trees

Fixed-Depth Incremental Merkle Tree

All note commitments are stored in an append-only, fixed-depth binary Merkle tree using Poseidon2 hashing. Each tree has a fixed depth of 24 (~16.7M leaves); empty subtrees are filled with precomputed zero hashes, so an append only recomputes the hashes along one path to the root. Leaves are never modified or deleted.

Multi-Tree Architecture

When the active tree fills up, a new tree is created (rollover). Input notes can reference commitments in any historical tree, so old notes remain spendable indefinitely. The active note tree accepts its 128 most recent roots. A finalized tree accepts only its final root, so spending an old note requires a membership path to that final root.

Tree Transitions in Circuits

When new notes are created, the ZK circuit proves that the tree state transition is valid, and the contract just verifies the proof and updates the stored root. This means the contract never has to compute Poseidon2 hashes onchain for leaf insertions.

Transaction Lifecycle

Transaction lifecycle swimlane: deposit, transfer, and forced withdrawal flows across User/SDK, TEE Server, and Smart Contract Transaction lifecycle swimlane: deposit, transfer, and forced withdrawal flows across User/SDK, TEE Server, and Smart Contract

Deposit (Public → Shielded)

Deposits are a 2-step process:
  1. User locks tokens: The user computes note commitments, encrypts the metadata using the dual-path ECDH scheme, and submits everything to the contract. The contract transfers the ERC-20 tokens and stores the pending deposit with replay protection.
  2. TEE processes the batch: The TEE detects pending deposits, generates a Groth16 proof verifying that commitments are correctly computed and the tree state transition is valid, then submits it onchain.
Cancellation: If the TEE doesn’t process a deposit within the cancel delay period, the depositor can reclaim their tokens.

Portal Deposit (Public → Shielded, Hidden Recipient)

A portal is a reusable public deposit address, like an exchange deposit address but self-custodial. The owner derives one address E, delegates it to a shared EIP-7702 implementation, and registers a public onchain binding H that commits to their account without naming it.
  1. Anyone funds it: Senders make ordinary ERC-20 transfers to E. They need no Privacy Boost software, and there is no per-deposit ceremony.
  2. Anyone sweeps it: The sweep call is permissionless. It measures the balance actually received, escrows it in the pool, snapshots the fee rate, and records the caller as the fee payee.
  3. The TEE credits the owner: A portal-deposit epoch proof credits the escrowed net amount as a new note, tying the credit to H without ever revealing the account.
Observers see the address, token, amount, timing, and sender, but never which shielded account was credited. Because crediting is decoupled from the deposit, the owner does not have to be online for any individual deposit. A swept deposit that is never credited can be cancelled by anyone after a cancel delay, which returns the gross amount to E.

Transfer (Shielded → Shielded)

Transfers move value between shielded notes. Sender identity, recipient identity, token type, and amounts are all hidden from onchain observers.
  1. User prepares: Select input notes, construct recipient and change notes, encrypt the output metadata, and authorize the operation with a signing key or an account-owner spend approval.
  2. User submits to TEE: The TEE checks the selected authorization and ciphertext integrity, then batches the transfer into an epoch.
  3. TEE submits epoch: The TEE generates a Groth16 proof covering the selected authorization, note existence, nullifier correctness, value conservation, and tree transition, then submits it onchain.
  4. Contract verifies: Checks the proof, marks nullifiers as spent, updates the tree root
How the recipient discovers the transfer: The TEE decrypts the output ciphertexts and indexes the new notes. The recipient queries their balance from the TEE indexer. For manual recovery, the recipient can also decrypt the onchain ciphertext independently using their viewing key.

Claimable Transfer (Shielded → Unregistered Wallet)

A claimable transfer sends shielded funds to an Ethereum wallet address that has no Privacy Boost account yet. An ordinary transfer needs the recipient’s privacy address, which a wallet address alone does not supply, so the flow splits into fund and claim.
  1. Fund: The sender funds a gift note bound to the recipient’s wallet address and a fresh hiding factor, through an ordinary shielded transfer. Onchain it is indistinguishable from any other transfer.
  2. Claim: The recipient registers Privacy Boost with that same wallet, proves control of it in zero knowledge, and the gift is re-minted as a normal shielded note they own. A wallet-recipient claim requires a registered signing key.
  3. Refund: The sender picks a refund deadline at funding time, and refunds with either a signing key or an account-owner approval once it passes. The deadline opens the refund path without expiring the recipient’s claim.
A claim and a refund consume the same gift note, so whichever settles first wins and the other can never settle. If the relay or TEE is unavailable, the recipient can still recover the funds through a permissionless public exit, which reveals the amount the way a forced withdrawal does.

Withdrawal (Shielded → Public)

Withdrawals use the same mechanism as transfers. The difference: instead of creating an output note, the contract sends ERC-20 tokens to a public address. Withdrawals are batched alongside transfers in the same epoch. The withdrawal’s first output is a public payout marker for the destination, token, and amount. It counts in the value balance but is never added to the note tree.

Forced Withdrawal (Emergency Exit)

Forced withdrawal is the self-custody escape hatch: exit the shielded pool without any TEE involvement.
  1. Reconstruct your notes: Scan onchain events and decrypt metadata with your viewing key
  2. Generate a ZK proof locally: Prove you own the notes and derive correct nullifiers (this runs on consumer hardware)
  3. Submit to the contract: The contract checks the exact live signing-key or dedicated size-1 approval leaf, verifies the proof, reserves the input commitments, and records the payout and fee before starting the delay.
  4. Execute after the delay: Any caller can execute the recorded payout if its nullifiers remain unspent. Execution uses the request-time authorization and fee, without checking the proof again. The account owner can cancel the request immediately.
Why the delay? It gives the account owner time to cancel unauthorized requests (e.g., from a compromised device) and allows the TEE to process any racing normal transactions.

Epoch Batching

Multiple user transactions are aggregated into a single epoch, one Groth16 proof covering the entire batch. This is a key design choice for throughput:
  • A single Groth16 proof verifies the entire epoch, so the fixed cost of proof verification is amortized across every transaction in the batch
  • The larger the batch, the lower the proof-verification overhead per transaction
  • At Base’s gas target, this enables 300+ sustained TPS; at the gas limit, 1,800+ TPS
Epochs are submitted when the batch reaches the configured size. Transfers are not instant; they settle when the epoch is submitted onchain. To bridge the gap between submission and on-chain settlement, the TEE exposes a preconfirmed state. Once a transfer is committed to an upcoming epoch, the TEE reserves its input nullifiers cluster-wide and materializes the output note in the indexer overlay. The SDK reports this state through the transaction status field and surfaces it as phase: 'spendable' so applications can chain the next private action (another transfer or a withdrawal) without waiting for the epoch to land. Preconfirmation is a soft confirmation: a reorg can still force the TEE to re-batch or fail the request, so clients keep polling until completed. Shields and unshields also pass through preconfirmed, but neither produces a spendable output, so that state is progress UI only. See Transaction Lifecycle for the full state machine. To prevent key-rotation attacks during an epoch, an epoch proof must reference a sufficiently fresh AuthRegistry root. When a root is replaced, the AuthRegistry records the block at which it was superseded, and the pool accepts an auth root only if it is current or still within a configured staleness window.

Fee Model

  • Ordinary deposits have no protocol deposit fee.
  • Portal deposits credit the gross swept amount minus the fee rate stored when the sweep was requested.
  • Transfers and withdrawals settled in an epoch each carry a private fee. The circuit enforces inputs = outputs + fee per operation and totals fees by token. The fee amount is set off-chain.
  • Public gift exits apply the current withdrawal fee subject to the authorized minimum net payout. Forced withdrawals snapshot the withdrawal fee at request time and deduct it at execution.

Private DeFi

Private DeFi lets users put shielded funds to work in external DeFi without publicly revealing their wallet identity. All DeFi activity is linked to Privacy Boost rather than to any individual user. The high-level flow:
  • Supply: move assets from your shielded balance into an approved vault and hold the resulting position privately.
  • Redeem: redeem that position back into assets in your shielded balance.
Use supported DeFi routes from your shielded balance. The external-call gateway supports configured ERC-4626 vault and token swap calls. Its owner installs allowed target and function-selector policies, optionally restricted to input and output tokens. The pool route must also be enabled. What stays private. The proof hides which input notes and account funded the action. The vault, token, amount, and timing are public, so external information can still correlate activity. Self-custody is preserved. The pool holds the resulting vault-share tokens. Once credited as ordinary notes, they use the same spend and forced-withdrawal paths as other registered tokens. The underlying assets are held by the external vault. The proof authorizes the exact target, calldata, expiry, and success and fallback receipts. A successful call creates a pending deposit for the measured output. An expired or safely reverted call creates the signed fallback deposit in the input token. A later deposit proof credits the resulting note, and delayed rescue handles an unprocessed gateway deposit.

Smart Contracts

PrivacyBoost, AuthRegistry, TokenRegistry, and AuditGateway use upgradeable implementations. PortalDelegate and ExternalCallGateway are separate contracts bound to a pool.

Coming Soon

More privacy-preserving capabilities are in development, each designed to preserve the same privacy and self-custody guarantees:
  • DeFi swaps: swap between tokens from a shielded balance without publicly exposing the user’s wallet identity.

Next Steps