Selective disclosure
Privacy Pools v2 supports per-transaction selective disclosure as a first-class primitive. The cryptographic foundation is the noteSecret generated for every output note: knowing that one secret lets you recompute exactly one note's commitment, and nothing else.
The setup
When a sender calls prepareTransfer, the SDK generates a fresh noteSecret for each output note and returns it on executeOptions.recipientPendingNotes[i].noteSecret. This secret:
- Is required to recompute the on-chain commitment (via the Poseidon chain documented in Commitments & nullifiers).
- Cannot be recovered from the on-chain state alone. It's known only to the sender at prepare time and (in discoverable mode) to the recipient via their viewing key.
- Is independent across transactions: disclosing one secret reveals exactly one note, never affects any other.
How a third party verifies a single payment
Given a receipt JSON (the same shape generate a receipt produces: note fields under receipt.note.*, the pool under receipt.pool.*), anyone with public RPC access can verify it client-side:
import { PoseidonHashService } from "@privacy-pools-v2/sdk";
// Minimal read-only ABI fragment; POOL_VAULT_ABI is not value-importable
// from the package entrypoint.
const POOL_VAULT_ABI = [
{
type: "function",
name: "commitments",
inputs: [{ name: "_commitmentHash", type: "uint256" }],
outputs: [{ name: "createdAt_", type: "uint256" }],
stateMutability: "view",
},
] as const;
const hash = await PoseidonHashService.create();
const note = receipt.note;
// 1. Recompute noteAddressHash
const noteAddressHash = hash.hash([note.ownerAddress, note.noteSecret]);
assert(BigInt(noteAddressHash) === BigInt(note.noteAddressHash));
// 2. Recompute commitment
const precommitment = hash.hash([noteAddressHash, note.tokenId, note.value, "0x0"]);
const commitment = hash.hash([precommitment, note.label]);
assert(BigInt(commitment) === BigInt(note.commitment));
// 3. Confirm the commitment is on-chain
const timestamp = await publicClient.readContract({
address: receipt.pool.poolAddress,
abi: POOL_VAULT_ABI,
functionName: "commitments",
args: [BigInt(commitment)],
});
assert(timestamp !== 0n);
If all three checks pass, the receipt is proof that: this exact value of this asset was inserted into the pool at this commitment, addressed to this recipient. The third party sees nothing else about the sender's wallet history.
What a third party does not learn
- Other payments the sender made.
- The sender's other notes or balances.
- The sender's nullifying / revocable / viewing keys.
- Other recipients in the same cycle / batch.
Mental model
Think of the noteSecret as a one-time viewing key scoped to a single payment. The user doesn't need to set anything up ahead of time, because every transfer the SDK builds already produces this secret as a byproduct.
For implementation, see Generate a payment receipt (usage) and receipt guide (end-to-end flow).