Skip to main content

Privacy Pools (0xbow)

Privacy Pools let users deposit ETH or ERC20 tokens and later withdraw to a different address without revealing which deposit funded the withdrawal. An Association Set Provider (ASP) layer lets pool operators exclude illicit deposits without breaking privacy for legitimate users. Nethereum provides a complete .NET SDK that is cross-compatible with the 0xbow TypeScript SDK.

dotnet add package Nethereum.PrivacyPools
dotnet add package Nethereum.PrivacyPools.Circuits # embedded circuit artifacts
dotnet add package Nethereum.ZkProofs.Snarkjs # proof generation (requires Node.js)

Native proof generation (no Node.js required — recommended for desktop/server):

dotnet add package Nethereum.CircomWitnessCalc       # native witness generation (C, P/Invoke)
dotnet add package Nethereum.ZkProofs.RapidSnark # native Groth16 proof generation (C++, P/Invoke)

For browser-based Blazor WASM applications, use Nethereum.ZkProofs.Snarkjs.Blazor instead. See the ZK Proof Demos guide for working examples of both approaches.

Create an Account​

Privacy Pool accounts are deterministically derived from a BIP-39 mnemonic. The same mnemonic always produces the same master keys, enabling recovery across devices.

var account = new PrivacyPoolAccount(mnemonic);
// account.MasterNullifier — from HD path m/44'/60'/0'/0/0
// account.MasterSecret — from HD path m/44'/60'/1'/0/0

The master keys generate unique deposit and withdrawal secrets for each pool scope and index, so one mnemonic can manage deposits across many pools.

Deploy a Privacy Pool​

Deploy the full contract stack (Entrypoint proxy, Pool, Verifiers, Poseidon libraries) in a single call.

Native ETH pool:

var deployment = await PrivacyPoolDeployer.DeployFullStackAsync(web3,
new PrivacyPoolDeploymentConfig { OwnerAddress = ownerAddress });

ERC20 token pool:

var deployment = await PrivacyPoolDeployer.DeployERC20FullStackAsync(web3,
new PrivacyPoolERC20DeploymentConfig
{
OwnerAddress = ownerAddress,
TokenAddress = erc20TokenAddress
});

Both deploy an Entrypoint (UUPS proxy), a pool contract (PrivacyPoolSimple for ETH, PrivacyPoolComplex for ERC20), Groth16 verifiers, and Poseidon hash libraries. The pool is automatically registered with the Entrypoint.

Deposit​

The PrivacyPool facade generates deterministic secrets from your mnemonic, computes the Poseidon commitment, and submits the deposit transaction.

var pp = PrivacyPool.FromDeployment(web3, deployment, mnemonic);
await pp.InitializeAsync();

// Native ETH deposit
var result = await pp.DepositAsync(Web3.Convert.ToWei(1), depositIndex: 0);

// ERC20 deposit (approve first)
await pp.ApproveERC20Async(tokenAddress, Web3.Convert.ToWei(1000));
var result = await pp.DepositERC20Async(tokenAddress, Web3.Convert.ToWei(100), depositIndex: 0);

Each deposit produces a DepositResult with the commitment details and transaction receipt. The depositIndex must be unique per scope — increment it for each new deposit.

Sync and Recover Accounts​

Recover all your accounts from on-chain events in a single call. SyncFromChainAsync fetches events, rebuilds the state tree, recovers your accounts, and builds the ASP tree.

var pp = PrivacyPool.FromDeployment(web3, deployment, mnemonic);
await pp.InitializeAsync();

var sync = await pp.SyncFromChainAsync();
// sync.PoolAccounts — recovered accounts with spendable balances
// sync.StateTree — Merkle tree of all commitments
// sync.ASPTree — ASP tree built from deposit labels
// sync.Deposits — all deposit events

For incremental sync after the first run, pass fromBlock and the existing state tree to avoid reprocessing old blocks.

Withdraw​

Two withdrawal modes are available:

ModeMethodPrivacyUse When
RelayPool.WithdrawAsync(...)High — relayer submits txProduction: your address stays hidden
DirectPool.WithdrawDirectAsync(...)Low — your address is msg.senderTesting or self-hosted scenarios

Both modes generate a Groth16 proof showing you know a commitment in the state tree AND the ASP's approved set, without revealing which one.

var circuitSource = new PrivacyPoolCircuitSource();
var proofProvider = pp.CreateProofProvider(new SnarkjsProofProvider(), circuitSource);

// Direct withdrawal
var result = await pp.Pool.WithdrawDirectAsync(
commitment, leafIndex, withdrawnValue, recipientAddress,
proofProvider, stateTree, aspTree);

Ragequit​

Exit the pool without ASP approval by proving knowledge of the deposit secrets. This reveals your commitment but guarantees you can always get your funds back.

var spendable = pp.GetSpendableAccounts();
var result = await pp.RagequitAsync(spendable[0], proofProvider);

ASP Tree Management​

The ASPTreeService builds and manages the Association Set Provider Merkle tree. Deposit labels become tree leaves, and the root is published on-chain for withdrawal proofs to reference.

var asp = pp.CreateASPTreeService();
asp.BuildFromDeposits(deposits);

// Publish root on-chain (requires POSTMAN role)
await asp.PublishRootAsync("bafybei...");

// Generate inclusion proof for a withdrawal
var (siblings, index) = asp.GenerateProof(label);

// Persist for later
var json = asp.Export();
// ...
asp.Import(json);

Run a Relayer​

The PrivacyPoolRelayer validates withdrawal proofs and submits transactions on behalf of users, preserving their privacy.

var verifier = new PrivacyPoolProofVerifier(withdrawalVkJson);
var relayer = new PrivacyPoolRelayer(web3, new RelayerConfig
{
EntrypointAddress = entrypointAddress,
PoolAddress = poolAddress,
FeeReceiverAddress = feeReceiver,
BaseFeeBps = 50 // 0.5% fee
}, verifier);
await relayer.InitializeAsync();

var result = await relayer.HandleRelayRequestAsync(request);

Cross-Compatibility​

The C# and TypeScript SDKs produce identical cryptographic outputs. This is validated by:

  • Unit tests — hardcoded value matching against the 0xbow TypeScript SDK
  • E2E cross-SDK tests — deposits from the TypeScript SDK are withdrawn by Nethereum and vice versa, with real Groth16 proofs verified on-chain. Covers both native ETH and ERC20 flows.

Next Steps​