Key Derivation

Nullmask derives all cryptographic keys deterministically from a single wallet signature. Since modern wallets use deterministic signatures (RFC 6979), all keys can be recovered by re-signing a fixed message.

Key derivation diagram
Key derivation: H denotes the Poseidon2T4 hash function

Viewing Key

circle-check
circle-check
circle-info

Algorithm 1: Derive Viewing Key

Input: wallet connection

Output: A Viewing Key (pk,nk,ivk,ovk)(\mathtt{pk}, \mathtt{nk}, \mathtt{ivk}, \mathtt{ovk})

  1. Request a personal_sign of the message EXPORT_VK_MESSAGE

  2. Recover pk\mathtt{pk} from the signature σ\sigma

  3. seedPoseidon2T4(σ)\mathtt{seed} \gets \operatorname{Poseidon2T4}(\sigma)

  4. nkPoseidon2T4(seed,1)\mathtt{nk} \gets \operatorname{Poseidon2T4}(\mathtt{seed}, 1)

  5. ivkPoseidon2T4(seed,2)\mathtt{ivk} \gets \operatorname{Poseidon2T4}(\mathtt{seed}, 2)

  6. ovkPoseidon2T4(seed,3)\mathtt{ovk} \gets \operatorname{Poseidon2T4}(\mathtt{seed}, 3)

  7. Return (pk,nk,ivk,ovk)(\mathtt{pk}, \mathtt{nk}, \mathtt{ivk}, \mathtt{ovk})

The viewing key grants view access to the account's transaction history. It remains stored in the proxy service and is never posted on-chain.

Receiving Key

circle-check
circle-info

Algorithm 2: Derive Receiving Key

Input: A Viewing Key (pk,nk,ivk,ovk)(\mathtt{pk}, \mathtt{nk}, \mathtt{ivk}, \mathtt{ovk})

Output: The corresponding Receiving Key (pk,pnk,ek)(\mathtt{pk}, \mathtt{pnk}, \mathtt{ek})

  1. pnkPoseidon2T4(nk)\mathtt{pnk} \gets \operatorname{Poseidon2T4}(\mathtt{nk})

  2. ekivkG\mathtt{ek} \gets \mathtt{ivk} \cdot \mathsf{G}

  3. Return (pk,pnk,ek)(\mathtt{pk}, \mathtt{pnk}, \mathtt{ek})

The receiving key is posted on-chain in the key registry. It enables anyone to send shielded funds to the corresponding address.

Receiving Key Hash

circle-info

Algorithm 3: Hash Receiving Key

Input: Receiving Key (pk,pnk,ek)(\mathtt{pk}, \mathtt{pnk}, \mathtt{ek})

Output: Receiving Key hash rk_hash\mathtt{rk\_hash}

  1. addresskeccak(pk)[12:]\mathtt{address} \gets \mathtt{keccak}(\mathtt{pk})[12:]

  2. bufferpk  (64 bytes)    address  (20 bytes)\mathtt{buffer} \gets \mathtt{pk} \; \text{(64 bytes)} \; || \; \mathtt{address} \; \text{(20 bytes)}

  3. Pad buffer with 9 zero bytes to length 93 bytes

  4. Split buffer into 3 parts of 31 bytes each

  5. Return Poseidon2T4(part1,part2,part3,pnk,ek.x,ek.y)\operatorname{Poseidon2T4}(\mathtt{part}_1, \mathtt{part}_2, \mathtt{part}_3, \mathtt{pnk}, \mathtt{ek.x}, \mathtt{ek.y})

The receiving key hash is a single field element that identifies the note owner. It is computed identically in the Noir circuit and the Solidity contract, enabling on-chain verification of key registry membership.

On-Chain Registration

The receiving key is registered on-chain via registerReceivingKey(). The contract:

  1. Recovers the Ethereum address from the public key components

  2. Verifies msg.sender matches the derived address

  3. Computes the receiving key hash using Poseidon2

  4. Inserts the hash into the key registry Merkle tree

This registration is verified in the shielded transfer circuit via a Merkle inclusion proof, ensuring the proxy cannot tamper with address-to-key mappings.

Key Storage

Key
Stored Where
Purpose

Viewing Key

Proxy (local storage)

Decrypt incoming/outgoing notes, derive nullifiers

Receiving Key

On-chain (key registry)

Enable others to send shielded funds

Access Token

HTTP-only cookie

Authenticate proxy requests

Last updated