Skip to main content

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).