Derive your protocol keys
What this lets you do
Produce the four protocol keys from one wallet signature. There is no server, no storage, and no enrollment ceremony, because the keys are a deterministic function of your wallet signature over a fixed message. If you lose the cache, you simply re-derive identical keys.
The four keys
privateNullifyingKey Used to derive nullifiers when spending. Highly sensitive: an attacker who obtains it can link your spends to known commitments, but cannot spend a note without also holding that note's secret and your revocable-key authorization.
privateRevocableKey Auth-policy material, rotatable via key rotation.
viewingPrivateKey Decrypts incoming note payloads. An attacker who obtains it can see your incoming receive history, but spending is unaffected.
viewingPublicKey The X25519 public key derived from viewingPrivateKey. It is safe to publish on-chain (see register viewing key).
Constraints & limits
- You must sign the exact EIP-712 payload the SDK's
deriveKeysFromSignatureexpects. The canonical reference isbuildSecretDerivationPayloadinv2-monorepo/apps/sample/src/keystore/secretDerivationPayload.ts, which is kept outside the SDK by design. - The keys are wallet-bound: the same wallet always produces the same keys, while a different wallet produces a different identity with different notes and a different viewing scope.
- Keep the derived keys on the client. Caching them in
sessionStoragespares a browser session from re-prompting the wallet, and you can always drop the cache and re-derive, since the same signature reproduces the same keys. Don't send them to a remote server.
What it unlocks next
- Register your viewing key so others can send you discoverable notes.
- All spending, transfer, withdraw, and discover operations require these keys.
How to use it
import { CryptoService, KeystoreManager } from "@privacy-pools-v2/sdk";
// Reference payload builder lives in the sample app, not the SDK (see above):
import { buildSecretDerivationPayload } from "./keystore/secretDerivationPayload.js";
const payload = buildSecretDerivationPayload(ownerAddress);
const signature = await walletClient.signTypedData({
account: ownerAddress,
domain: payload.domain,
types: { SecretDerivation: payload.types.SecretDerivation },
primaryType: payload.primaryType,
message: payload.message,
});
const keystoreManager = KeystoreManager.fromSignature(
{
signature,
signerAddress: ownerAddress,
addressHash: payload.message.addressHash,
revocableKeyIndex: "0x0",
},
{ cryptoService: new CryptoService() },
);
const privateNullifyingKey = keystoreManager.getPrivateNullifyingKey();
const privateRevocableKey = keystoreManager.getPrivateRevocableKey();
const { privateKey: viewingPrivateKey, publicKey: viewingPublicKey } =
keystoreManager.getViewingKeyPair();
Recover on a new device
Because the keys come only from your wallet signature, recovering an account on a new device (or after a sessionStorage wipe) is just running this derivation again. There is no extra state to back up. Two things differ from a first-time derivation:
- Rotation index: if the account ever rotated its revocable key, re-derive at its current
revocableKeyIndexrather than0x0. Reuse the index your app persisted, or recover it on-chain withsession.discoverRevocableKeyIndex(...), which scans candidate indices against the stored auth digest. - Notes: the keys alone don't carry your note history. After rebuilding the session, call
discoverNotes()to repopulate it from chain.
The same wallet on a phone and a laptop derives the same keys and sees the same notes, so multi-device access needs no syncing beyond that re-discovery.
Behind the scenes
Primitives EIP-712 typed-data signing, two-stage HKDF-SHA256 derivation, and X25519 ECDH public-key derivation
SDK source
v2-monorepo/packages/sdk/src/services/CryptoService.ts (deriveKeysFromSignature)Where Fully client-side. The wallet signature happens via your provider; the SDK requires a standard 65-byte ECDSA signature