Skip to main content
Version: 0.3.0

Keys & Addresses

Each ZKP account has two keys on different elliptic curves, plus two corresponding addresses.

Dual-key model

KeyCurvePurpose
ESK (Encryption Secret Key)GrumpkinElGamal decryption + ZK proof witness
CSK (Controller Spending Key)secp256k1EIP-712 controller authorization signatures

By default, both are derived from the same seed (BIP-39 mnemonic or EIP-712 signature) using domain-separated key derivation. They're independent -- compromising one doesn't reveal the other.

The CSK can also be a separate external signer instead of a derived key. For example, a Fireblocks custodial account, a hardware wallet, or any EVM wallet can serve as the controller signer. In this case, only the ESK is derived from the mnemonic -- the CSK signing is delegated to the external signer. See Account Creation for details.

Why two curves?

  • Grumpkin is optimized for zero-knowledge circuits -- think of it as the ZK world's equivalent of secp256k1. You never interact with it directly; the SDK handles it.
  • secp256k1 is the EVM-native curve -- compatible with ecrecover, MetaMask, Ledger, and all standard wallets. Using it for controller signing means any existing wallet can authorize transfers.

ZK address

The encryption public key (EPK) is encoded as a bech32m address:

zk1qw508d6qejxtdg4y5r3zarv...
  • HRP: zk
  • 1 is the bech32m separator (not a version number)
  • Payload: version byte + compressed EPK (33 bytes)
import { epkToAddress, addressToEpk, isValidAddress } from '@cardinal-cryptography/core'

const address = epkToAddress(epk) // Point -> 'zk1...'
const epk = addressToEpk('zk1...') // 'zk1...' -> Point
isValidAddress('zk1...') // boolean

Controller address

The controller address is a standard EVM address that the contract uses to verify EIP-712 authorization signatures via ecrecover.

When CSK is derived from a mnemonic:

import { cskToAddress } from '@cardinal-cryptography/core'
const controllerAddress = cskToAddress(csk) // '0x...'

When using an external signer (e.g., Fireblocks, MetaMask), the controller address is simply the signer's EVM address.

Key derivation paths

From a mnemonic:

mnemonic → BIP-39 seed
→ domain "zkprivacy-eb-encryption-key-v1" → ESK (mod Grumpkin order)
→ domain "zkprivacy-eb-controller-key-v1" → CSK (mod secp256k1 order)

From an EIP-712 signature (linked wallet flow):

signature bytes → SHA-256 → seed
→ domain "zkprivacy-eb-encryption-key-v1" → ESK (mod Grumpkin order)
→ domain "zkprivacy-eb-controller-key-v1" → CSK (mod secp256k1 order)

The same domain-separated derivation is used in both cases -- the only difference is the seed source (mnemonic vs signature hash).

Account index is supported: append /{index} to the domain string for indices > 0.