Skip to Content
Key Management

Key Management

Every user in the Anoma Pay protocol is represented by four cryptographic key pairs. Together they form a keyring (UserKeyring). Understanding what each key does helps you reason about which parts of the keyring need to be available at runtime and which require secure storage.

The four key pairs

Key pairTypeRole
authorityKeyPairsecp256k1Signs the action tree to authorize resource consumption
nullifierKeyPaircustomProves the right to spend (nullify) a resource
encryptionKeyPairsecp256k1Derives per-resource encryption keys so only the owner can decrypt their balances
discoveryKeyPairsecp256k1Lets the Indexer route incoming resources to the right user

Authority key pair

The authority key pair signs the action tree — a Merkle root computed over the set of resources being consumed and created in a transaction. Without a valid signature from the authority key, the backend will reject the transaction.

keyring.authorityKeyPair.publicKey // Uint8Array (33 bytes, compressed secp256k1) keyring.authorityKeyPair.privateKey // Uint8Array (32 bytes)

Nullifier key pair

The nullifier key pair is unique: its “commitment” (cnk) is the SHA-256 hash of the private key (nk). This commitment is baked into each resource at creation time. When spending a resource, the protocol derives a nullifier from nk — a unique tag that proves consumption without revealing which resource was spent.

keyring.nullifierKeyPair.nk // Uint8Array — the private nullifier key keyring.nullifierKeyPair.cnk // Uint8Array — SHA256(nk), embedded in resources

Encryption key pair

When a resource is created for you, its payload is encrypted to your encryption public key. The SDK uses the corresponding private key to decrypt resource blobs fetched from the Indexer, recovering the token type, amount, and other metadata.

Discovery key pair

The discovery private key is registered with the Indexer via IndexerClient.addKeys(). The Indexer uses the corresponding public key to tag incoming resources so they can be retrieved later.

Key derivation

All four key pairs are derived from a single 32-byte seed using HMAC-SHA256 with separate domain strings:

KeyDomain string
AuthorityANOMA_AUTHORITY_KEY
NullifierANOMA_NULLIFIER_KEY
EncryptionANOMA_STATIC_ENCRYPTION_KEY
DiscoveryANOMA_STATIC_DISCOVERY_KEY

A single seed reproducibly derives the entire keyring. Losing the seed means losing access to all resources derived from it.

Creating a keyring

Random (new user)

import { createUserKeyring } from "@anomaorg/anoma-app-sdk"; const keyring = createUserKeyring(); // uses crypto.getRandomValues internally

From a seed (deterministic)

Pass any 32-byte Uint8Array as a seed. The SDK applies the PRF to each domain separately:

import { createUserKeyring } from "@anomaorg/anoma-app-sdk"; const seed: Uint8Array = /* 32 bytes */; const keyring = createUserKeyring(seed);

From a wallet signature (IKM)

Derive a seed by having the user sign a fixed message with their EVM wallet, then pass that signature as input key material (IKM). HKDF-SHA256 is applied internally with the salt "anoma-pay:keyring-seed":

import { createUserKeyringFromIkm } from "@anomaorg/anoma-app-sdk"; // ikm can be any byte sequence — e.g. a wallet signature over a fixed message const ikm: Uint8Array = /* bytes derived from wallet signature */; const keyring = createUserKeyringFromIkm(ikm);

The recommended approach for browser applications. The passkey PRF extension produces deterministic output from the device authenticator:

import { createUserKeyringFromPasskey } from "@anomaorg/anoma-app-sdk"; // credential must have been obtained with the PRF extension enabled const keyring = createUserKeyringFromPasskey(credential);

This throws a descriptive error if the authenticator does not support the WebAuthn PRF extension.

Extracting public keys

To receive tokens, a user shares their public keys. Extract them from the full keyring:

import { createUserKeyring, extractUserPublicKeys, } from "@anomaorg/anoma-app-sdk"; const keyring = createUserKeyring(); const publicKeys = extractUserPublicKeys(keyring); // { // authorityPublicKey: Uint8Array (33 bytes) // discoveryPublicKey: Uint8Array (33 bytes) // encryptionPublicKey: Uint8Array (33 bytes) // nullifierKeyCommitment: Uint8Array (32 bytes) // }

Pay Address encoding

A Pay Address encodes the four public keys into a single 180-character Base64URL string for sharing. The raw bytes are assembled in this fixed order before encoding:

FieldSize (bytes)
Authority Public Key33
Discovery Public Key33
Encryption Public Key33
Nullifier Key Commitment32
CRC32 Checksum4
Total135

The CRC32 checksum covers the first 131 bytes and is used to catch transcription errors. It does not provide cryptographic security.

Encoding

import { createUserKeyring, encodePayAddress, extractUserPublicKeys, } from "@anomaorg/anoma-app-sdk"; const keyring = createUserKeyring(); const payAddress = encodePayAddress(extractUserPublicKeys(keyring));

There is also a convenience helper that combines the two steps:

import { createUserKeyring, getPayAddressFromKeyring, } from "@anomaorg/anoma-app-sdk"; const keyring = createUserKeyring(); const payAddress = getPayAddressFromKeyring(keyring);

Decoding

Decoding validates the length and CRC32 checksum before returning the public keys:

import { decodePayAddress } from "@anomaorg/anoma-app-sdk"; const keys = decodePayAddress(payAddress); // keys.authorityPublicKey — Uint8Array (33 bytes) // keys.discoveryPublicKey — Uint8Array (33 bytes) // keys.encryptionPublicKey — Uint8Array (33 bytes) // keys.nullifierKeyCommitment — Uint8Array (32 bytes)

decodePayAddress throws if the string is empty, the decoded byte length is not 135, or the CRC32 checksum does not match.

Validation

Use isValidPayAddress to get a boolean result without throwing:

import { isValidPayAddress } from "@anomaorg/anoma-app-sdk"; if (!isValidPayAddress(userInput)) { // show a validation error in the UI }

Serialization

Keyrings should be persisted across sessions. The serialized format hex-encodes all private and public key bytes into a JSON object:

import { serializeUserKeyring, deserializeUserKeyring, } from "@anomaorg/anoma-app-sdk"; // Serialize — returns a JSON string containing private key material const json = serializeUserKeyring(keyring); // Restore const restored = deserializeUserKeyring(json);

The serialized keyring contains all private key bytes in plaintext JSON. Treat it with the same care as a private key. Store it in sessionStorage (cleared on tab close) or an encrypted vault. Never persist it to localStorage unencrypted or transmit it over the network.

Last updated on