Skip to main content
Version: 0.3.0

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:

  1. Mnemonic → seed via PBKDF2-SHA512 (standard BIP-39).

  2. 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_ORDER
    • signing_key = SHA-256("zkprivacy-eb-signing-key-v1" ∥ seed) mod GRUMPKIN_CURVE_ORDER
    • viewing_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 with 1.

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