Key Management
Users derive all keys from a single BIP-39 mnemonic. The mnemonic produces a seed, from which several independent keys are derived as siblings via domain-separated SHA-256 — not in a parent-child hierarchy. This way knowing one key (e.g. an ESK shared with a relayer for proof generation) does not let an attacker derive any of the others.
The keys derived from one seed:
- Encryption Secret Key (ESK) — Grumpkin scalar. Decrypts incoming balances and serves as the witness in ZK proofs. The Encryption Public Key (EPK) is
ESK · G. The EPK is the user's on-chain identity — balances are stored under the hash of the EPK, not under an EVM address. To send someone funds you need their EPK. - Controller Spending Key (CSK) — secp256k1 scalar. Authorizes balance-decreasing operations on-chain via EIP-712 signatures. The Controller Public Key (CPK) is the corresponding Ethereum address.
- signing key — Grumpkin scalar. Reserved (Schnorr attestations, future use).
- viewing key — Grumpkin scalar. Reserved (compliance-scoped balance viewing, future use).
Spending from your own balance requires both ESK (proof witness) and CSK (controller signature) — neither alone is sufficient. This separation enables custody patterns where a relayer holds the ESK for proof generation while a custody provider (Fireblocks, Safe multisig, etc.) holds the CSK for spend authorization.
An alternative linked mode sources the seed from an EIP-712 signature produced by an existing EVM wallet (e.g. MetaMask). The signature bytes are hashed (SHA-256) into seed bytes, then the same sibling derivation runs on top — letting users tie their privacy identity to an Ethereum account without managing a separate mnemonic. In institutional mode the CSK is generated and held externally instead of derived from the seed; only the ESK comes from the mnemonic.
Derivation
Starting from a BIP-39 mnemonic:
-
Mnemonic → seed via PBKDF2-SHA512 (standard BIP-39).
-
Each Grumpkin-curve key is
SHA-256(domain ∥ seed) mod GRUMPKIN_CURVE_ORDER, with the domain identifying the role:ESK = SHA-256("zkprivacy-eb-encryption-key-v1" ∥ seed) mod GRUMPKIN_CURVE_ORDERsigning_key = SHA-256("zkprivacy-eb-signing-key-v1" ∥ seed) mod GRUMPKIN_CURVE_ORDERviewing_key = SHA-256("zkprivacy-eb-viewing-key-v1" ∥ seed) mod GRUMPKIN_CURVE_ORDER
If a derived Grumpkin scalar is zero (probability
≈ 2^-256), it is replaced with1. -
The CSK uses the same KDF construction reduced modulo the secp256k1 group order:
CSK = SHA-256("zkprivacy-eb-controller-key-v1" ∥ seed) mod SECP256K1_CURVE_ORDER
Multiple accounts are supported via domain separation: account index N > 0 uses "zkprivacy-eb-{role}-v1/{N}" (the unindexed string is shorthand for index 0).
Implementation: deriveSibling in packages/core/src/keys/derivation.ts (Grumpkin keys), deriveCSK in packages/core/src/keys/controller.ts (CSK).