Skip to main content

Share a single-note secret

What this lets you do

Reveal exactly one incoming note to a third party, like an auditor or accountant, without sharing your viewing private key. The mechanism is the per-note ephemeral X25519 keypair that each Note event carries: ECDH between your viewingPrivateKey and the note's ephemeralPubKey produces a shared secret that decrypts only that one note's encrypted payload. When you hand that shared secret to a third party, they can decrypt that single note while every other incoming note stays private.

This complements the sender-side payment receipt flow: a receipt proves "I sent X" from the sender's vantage, while the single-note secret proves "I received X" from the recipient's vantage. Both result in single-note disclosure without wholesale viewing-key disclosure.

Constraints & limits

  • Discoverable-mode notes only. The decryption target is the encrypted payload in the on-chain Note event. Out-of-band notes don't have such a payload, so their selective disclosure uses the noteSecret directly via the receipt path.
  • Per-note granularity. Each Note event has its own ephemeralPubKey, which means the shared secret discloses exactly one note. Disclosing a batch means sharing one secret per note.
  • You're sharing a 32-byte symmetric secret. Treat it with the same caution as the noteSecret, because anyone who holds it can decrypt the corresponding payload.
  • Doesn't disclose nullifier-derivation material. The auditor sees the note's content (value, tokenId, label, plus the noteSecret) but cannot derive the nullifier without your privateNullifyingKey. That means they can't tell whether the note has been spent. Pair this with verify-spent if they need to know.

What it unlocks next

  • Recipient-side compliance: a user can prove to their tax authority "I received this specific shielded payment from this org" without enabling them to inspect their broader receive history.
  • Selective audit trails for organizations that receive payments (donations, grants, vendor refunds) and need per-receipt disclosure rather than wallet-level.

How to use it

import { CryptoService } from "@privacy-pools-v2/sdk";

// 1. Identify the specific incoming note by its commitment.
const target = (await session.exportAccount()).notes
.find((n) => n.commitment === claimedCommitment);
if (!target) throw new Error("note not found: " + claimedCommitment);

// 2. Fetch the on-chain Note event to recover the ephemeralPubKey.
// Note events are indexed by `hint`, not commitment, and the note's
// createdAtBlock holds a timestamp, not a block number. Locate the
// event through the note's creation transaction instead.
const receipt = await publicClient.getTransactionReceipt({ hash: target.txHash });
const noteEvents = parseEventLogs({ abi: [NOTE_EVENT_ABI], logs: receipt.logs });
const { ephemeralPubKey, encryptedPayload } = decodeNoteData(noteEvents[0].args.data);

// 3. Derive the shared secret with the recipient's viewing private key.
const crypto = new CryptoService();
const sharedSecret = crypto.ecdh(viewingPrivateKey, ephemeralPubKey);

// 4. Ship a disclosure bundle.
const disclosure = {
type: "privacy-pools-v2-shared-secret-disclosure",
pool: { chainId, poolAddress },
note: { commitment: target.commitment, txHash: noteEvents[0].transactionHash },
encryptedPayload, // already on-chain; included for convenience
sharedSecret, // the disclosure
};
// Auditor uses sharedSecret to decrypt encryptedPayload via the SDK's NotePayload codec.

Behind the scenes

SDK utility CryptoService.ecdh(viewingPrivateKey, ephemeralPubKey), which performs X25519 ECDH
Decryption The auditor passes the shared secret and the on-chain encryptedPayload to the SDK's NotePayload codec to recover (noteSecret, value, tokenId, label).
What's not shared Your viewingPrivateKey stays on your device. Future incoming notes use different ephemeral pubkeys, so each one produces a different shared secret.