Skip to main content

ADR-015: Key Naming Conventions and Key Architecture

StatusImplemented
Date2026-04-01

Context

The current codebase uses BPK (Balance Public Key) and BSK (Balance Secret Key) terminology inherited from early prototypes. These names are misleading: the keys are used for ElGamal encryption, not as balance identifiers. The name "balance key" conflates the key's role (encryption) with the data it protects (balances).

Additionally, the system is evolving toward institutional custody via Fireblocks. Fireblocks cannot operate on exotic curves (Grumpkin), so spend authorization must use standard secp256k1 ECDSA signatures. This requires introducing a new controller key concept (that would make the original BPK/BSK names even more confusing).

Finally, a future UTXO-based shielded layer is planned. Its key requirements differ from the current encrypted balance layer, and we need to decide upfront whether keys should be shared or isolated.

This ADR establishes:

  1. Consistent naming for all key types across the three-layer architecture.
  2. Layer names and function naming conventions.
  3. How the controller key (specified in ADR-014) fits into the overall key hierarchy.
  4. Rules for when to use full vs. compressed key representations.
  5. Rough key architecture for the future shielded layer.

Proposal

1. Rename BPK/BSK → EPK/ESK

Old nameNew nameCurveRole
BSK (Balance Secret Key)ESK (Encryption Secret Key)GrumpkinElGamal decryption, proof witness
BPK (Balance Public Key)EPK (Encryption Public Key)GrumpkinElGamal encryption target, on-chain account identifier

Rationale: "Encryption" accurately describes the key's function.

The rename is cosmetic in Solidity (compressed bytes32 storage keys are curve-agnostic) but affects TypeScript interfaces, documentation, and developer mental models.

2. Layer Names and Function Naming

The three layers are named: Public, Encrypted, Shielded.

The current codebase uses "concealed" (e.g., concealedTransfer, concealAmount). This ADR renames to "encrypted" — more technically accurate since the mechanism is ElGamal encryption.

Naming convention:

  • All transfers use explicit source and destination: {source}To{destination}Transfer — e.g., publicToEncryptedTransfer, encryptedToPublicTransfer
  • Within-layer shorthand: {layer}Transfer — e.g., encryptedTransfer, shieldedTransfer (source and destination are the same layer)

Full cross-layer function matrix:

From → ToFunction name
Public → EncryptedpublicToEncryptedTransfer
Encrypted → PublicencryptedToPublicTransfer
Encrypted → EncryptedencryptedTransfer
Public → ShieldedpublicToShieldedTransfer
Shielded → PublicshieldedToPublicTransfer
Shielded → ShieldedshieldedTransfer
Encrypted → ShieldedencryptedToShieldedTransfer
Shielded → EncryptedshieldedToEncryptedTransfer

Rename map (current → new):

CurrentNew
concealAmountpublicToEncryptedTransfer
concealAmountWithSigpublicToEncryptedTransferWithSig
revealAmountencryptedToPublicTransfer
concealedTransferencryptedTransfer
setAutoconcealsetAutoEncrypt
activatePendingactivatePending (unchanged)
registerEpkregisterEpk (unchanged)

Balance queries (encryptedBalanceOf, pendingBalances, pendingActive) are already consistent and remain unchanged.

3. Three-Layer Key Architecture

┌───────────────────────────────────────────────────────────────────────┐
│ PUBLIC LAYER │
│ Regular EVM account (EOA / smart account) │
│ Key: secp256k1 private key → Ethereum address │
│ Role: Hold ERC-20 tokens, set auto-encrypt, gas payment │
│ │
│ NOTE: For privacy, should NOT be the same key as CPK. │
│ Reusing the same key for public EVM and controller roles allows │
│ observers to link auto-encrypt targets to spend authority (and more │
│ specifically — to tell which accounts auto-encrypt to their own │
│ address, and which to an external one). │
├───────────────────────────────────────────────────────────────────────┤
│ ENCRYPTED LAYER │
│ Encrypted on-chain balances (ElGamal on Grumpkin) │
│ │
│ ESK (Encryption Secret Key) — Grumpkin scalar │
│ └── EPK = ESK · G — Grumpkin point │
│ │
│ Derived but currently unused (reserved for future use): │
│ Signing Key = HMAC(ESK, "signing") — Grumpkin scalar │
│ Viewing Key = HMAC(ESK, "viewing") — Grumpkin scalar │
│ │
│ CPK (Controller Public Key) — secp256k1 / Ethereum address │
│ └── CSK (Controller Secret Key) — secp256k1 private key │
│ Role: Authorizes all account actions via EIP-712 signature │
│ (spends, activatePending, changeController) │
│ Holder: Fireblocks (institutional) or user (self-custody) │
│ Bound to EPK at registration; mandatory for all accounts │
├───────────────────────────────────────────────────────────────────────┤
│ SHIELDED LAYER (future) │
│ UTXO-based privacy │
│ Separate key set — does NOT reuse encrypted-layer keys │
│ See §7 for rough spec │
└───────────────────────────────────────────────────────────────────────┘

4. Controller Key (CPK/CSK)

The controller mechanism is fully specified in ADR-014. This section summarizes how it fits into the key hierarchy and naming.

Note: ADR-014 uses the pre-rename terminology (BPK/BSK). The mapping below applies once the rename in §1 is implemented.

Summary: The controller is an EVM address bound to each EPK via registerEpk (called registerBpk in ADR-014, pre-rename) (ADR-014). It authorizes all account actions — spends (encryptedTransfer, encryptedToPublicTransfer), pending balance activation (activatePending), and controller rotation (changeController) — via EIP-712 signatures validated on-chain using OpenZeppelin's SignatureChecker (supporting both EOA and ERC-1271 smart contract wallets). Registration is mandatory: an EPK cannot perform outgoing operations until a controller is bound. See ADR-014 for storage mappings, function signatures, EIP-712 type hashes, and security analysis.

Naming in this ADR's terminology:

ADR-014 termThis ADR's nameDescription
Controller addressCPK (Controller Public Key)The Ethereum address registered on-chain
Controller private keyCSK (Controller Secret Key)secp256k1 key held by Fireblocks or user

Key separation property: The CSK is not derived from the ESK. It is an independent secp256k1 key. This separation enables the Fireblocks custody model where the relayer holds the ESK (for proof generation) while Fireblocks holds the CSK (for spend authorization). Neither party alone can steal funds — see ADR-014 §Security properties for the full threat model.

Authorization model per operation:

OperationESK (proof)CSK (signature)Notes
registerEpkYesESK ownership proof binds controller; anyone can submit tx
activatePendingYesController signature sufficient; no proof needed
encryptedTransferYesYesDual authorization: proof + controller signature
encryptedToPublicTransferYesYesDual authorization: proof + controller signature
changeControllerYesCurrent controller authorizes rotation
publicToEncryptedTransferIncoming deposit; no auth from EPK holder needed

5. Key Derivation

Current derivation from ESK:

Self-custody mode — single mnemonic, everything derived:

mnemonic (BIP-39)
├── ESK — Grumpkin scalar (ElGamal encryption/decryption)
│ └── EPK = ESK · G (on-chain identifier)
├── signingKey — Grumpkin scalar (reserved, unused)
├── viewingKey — Grumpkin scalar (reserved, unused)
└── CSK — secp256k1 private key (controller)
└── CPK = CSK → Ethereum address (registered on-chain)

All keys are derived as siblings from the seed via domain-separated KDFs, not parent-child. This ensures that having one key (e.g., ESK given to a relayer) does not allow deriving the others (e.g., viewingKey for auditors). The exact KDF and domain strings are an implementation detail; indexed accounts (HD-wallet style derivation for multiple accounts from one seed) will be covered in a separate ADR.

Institutional mode — ESK derived from mnemonic, CSK externally managed:

mnemonic (BIP-39) External (Fireblocks / Safe / etc.)
└── ESK → EPK (same as above) CSK → CPK
Bound to EPK via registerEpk (ADR-014)

The signingKey and viewingKey are not consumed by any operation today. They are reserved for future use (e.g., Schnorr attestations, compliance-scoped balance viewing). Note: the current SDK (keys.ts) derives them as children of ESK — this should be changed to sibling derivation from the seed as shown above.

In self-custody mode, the CSK is derived from the same mnemonic as the ESK, using domain separation to produce a secp256k1 key. This mirrors the pattern in stealth/keys.ts which derives both curve types from a single master secret. Users back up one mnemonic.

In institutional mode, the CSK is an independent key generated and held by the custody provider (Fireblocks, a Safe multisig, etc.). It is not derived from the mnemonic. The CPK is registered on-chain via registerEpk (ADR-014), which requires a ZK proof of ESK ownership to prevent unauthorized controller assignment.

6. Full Point vs. Compressed Representation

An EPK is a Grumpkin curve point with two 256-bit coordinates (x, y). It can be compressed to a single bytes32 by storing only x plus one bit of y-parity — halving calldata cost. On-chain decompression (Tonelli-Shanks square root) is prohibitively expensive (~100k gas), but the contract never needs to decompress: compressed points serve only as mapping keys.

Currently, all EBEMT public functions accept full Point calldata and compress internally. This wastes 32 bytes of calldata for functions that never use the y-coordinate beyond compression. We recommend switching proof-free functions to accept CompressedPoint directly, and keeping full Point only where the ZK verifier needs both coordinates as public inputs.

Code changes required:

  • Solidity: Change signatures of publicToEncryptedTransfer, activatePending, changeController, setAutoEncrypt from Point calldata bpk to CompressedPoint bpk; remove internal compressBpk() calls in those functions.
  • SDK: compressBpkHex() is already available. Wallet methods calling proof-free functions switch from passing {x, y} to passing the compressed bytes32. Transparent to end users.
  • No circuit changes — circuits always use full (x, y).
ContextRepresentationRationale
ZK circuit inputsFull (x, y)Circuits operate on field elements
ElGamal encryption/decryptionFull (x, y)Scalar multiplication needs both coordinates
On-chain storage keys / eventsCompressed bytes32Gas-efficient; y-parity (1 bit) + x (255 bits)
On-chain args (with ZK proof)Full Point (x, y)Verifier needs both coordinates as public inputs
On-chain args (no ZK proof)Compressed bytes32Saves 32 bytes calldata; no decompression needed
SDK public APIString (epk0x...)Opaque identifier; SDK decompresses internally when needed

Rule: Full (x, y) is an internal detail — only ZK circuits and the SDK internals work with raw coordinates. Everything else uses compressed form: CompressedPoint on-chain, string identifiers in the SDK public API (similar to how Ethereum uses 0x... addresses, not raw secp256k1 points). The SDK public API uses the format epk0x{hex} where {hex} is the compressed point (32 bytes, hex-encoded). This prefix makes EPK addresses self-describing and consistent with Ethereum's 0x convention.

Applied to contract functions:

FunctionProof?EPK arg type
registerEpkYesPoint
encryptedTransferYesPoint (sender and recipient)
encryptedToPublicTransferYesPoint
publicToEncryptedTransferNoCompressedPoint
activatePendingNoCompressedPoint
changeControllerNoCompressedPoint
setAutoEncryptNoCompressedPoint

7. Shielded Layer Key Architecture (Rough Spec)

The shielded layer is a future UTXO-based privacy system similar to Railgun. It is architecturally separate from the encrypted layer.

Key Isolation

Shielded-layer keys are not derived from encrypted-layer keys (ESK/EPK). Reasons:

  • Different trust model: Encrypted layer allows a relayer to hold the encryption key; shielded layer requires spend authority to be tightly held.
  • Different cryptographic requirements: The shielded layer may use different commitment schemes, nullifier derivation, and note encryption.
  • Blast radius: Compromise of one layer's keys should not affect the other.

Key Types (Tentative)

KeyCurveRole
Shielded Spending KeyTBD (see below)Nullifier derivation, note spending
Shielded Viewing KeyDerived from spending keyNote decryption, balance scanning
Spend authorization mechanism (native Grumpkin vs. ECDSA-in-ZK for custody compatibility) is deferred to a dedicated shielded layer ADR.

Consequences

What becomes easier

  • Naming clarity: EPK/ESK immediately communicates "encryption key," reducing onboarding confusion.
  • Clean separation: Three-layer architecture with isolated key sets makes security analysis tractable — each layer can be audited independently.
  • Future flexibility: Shielded layer key architecture is isolated, leaving room for future decisions on spend authorization mechanism.

What becomes harder

  • Rename effort: BPK/BSK → EPK/ESK touches TypeScript interfaces, Solidity mappings/events, documentation, and developer tooling.
  • No key reuse across layers: Users need separate key sets for encrypted and shielded layers, increasing key management burden.