Keys
Privacy Pools v2 splits private-key responsibilities across three keys, all derived from one EIP-712 signature made with your wallet. Because each key does exactly one job, you can disclose viewing access without disclosing spending capability.
The three keys
Why three keys?
- Read-only delegation: you can hand an accountant your viewingPrivateKey for read-only access, so they see incoming transfers but cannot spend.
- Key rotation: you can rotate the revocable key without re-depositing or losing notes, because the revocableKeyIndex bookkeeping keeps derivation consistent across rotations.
- Reduced blast radius: when a key does leak, the blast radius stays small, since a compromised viewing key exposes history rather than funds.
Derivation (canonical)
The three private keys come from one EIP-712 signature (the viewingPublicKey is then computed from viewingPrivateKey, not derived independently). The canonical derivation is the SDK's two-stage HKDF, invoked via cryptoService.deriveKeysFromSignature(...) (see v2-monorepo/packages/sdk/src/services/CryptoService.ts). At a high level it runs:
1. payload = buildSecretDerivationPayload(ownerAddress);
2. signature = await wallet.signTypedData(payload);
3. rootSecret = HKDF(sha256, ikm=r, salt=signerAddress, info=ROOT_DERIVATION_INFO, L=32);
4. appSecret = HKDF(sha256, ikm=rootSecret, salt=APP_IDENTIFIER, info=APP_DERIVATION_INFO, L=32);
5. (privateNullifyingKey, privateRevocableKey, viewingPrivateKey)
= deriveKeys(appSecret, revocableKeyIndex);
6. viewingPublicKey = x25519.getPublicKey(viewingPrivateKey);
You don't need to implement HKDF yourself, because cryptoService.deriveKeysFromSignature performs the two-stage HKDF for you. The only thing you supply is the EIP-712 payload. Use the buildSecretDerivationPayload reference helper at v2-monorepo/apps/sample/src/keystore/secretDerivationPayload.ts, noting that it lives in the sample app rather than the SDK package.
Where the keys live
- Wherever your app runs, keys are derived on demand and held in memory. The SDK never persists them anywhere. Its
LocalStorageServicestores note state, not keys, and any key caching is the integrator's responsibility. Because re-signing the same message reproduces the same keys, prefer re-deriving over storing them. - On-chain, only the viewingPublicKey is stored, and only after registration via
session.registerKeystore()(two transactions, a one-time gas cost that varies with network conditions).
Disclosure scenarios
- Selective per-payment disclosure: you share the noteSecret of one specific payment, and the auditor sees exactly that payment. See Selective disclosure.
- Full receive-history disclosure: you share the viewingPrivateKey, and the auditor sees every incoming transfer. Your spending capability is unaffected.
- Wallet recovery: re-signing the EIP-712 message from the same wallet re-derives the same keys.