Cryptography Reference
All cryptographic primitives and parameters used in the system.
Curves
Grumpkin is the only curve used across all layers. It is an embedded curve over BN254's scalar field, which makes it efficient to operate on inside circuits that use BN254 as the proof system's native field.
| Parameter | Value |
|---|---|
| Equation | y² = x³ − 17 |
| Base field | BN254 scalar field (Fr): 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001 |
| Curve order | BN254 base field (Fq): 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47 |
| Generator | G = (1, 0x02cf135e7506a45d632d270d45f1181294833fc48d823f272c) |
| Cofactor | 1 |
The identity element is encoded as (0,0) throughout the codebase — this is not a point on the curve, but is special-cased as the identity in all Solidity, Noir, and TypeScript code.
Proof System
UltraHonk via Noir and Barretenberg (bb.js). All circuits compile to constraint systems of size 2^15 (32,768 gates).
| Circuit | Public inputs |
|---|---|
| Transfer | 23 |
| Unshield | 12 |
| Stake | 18 |
| Unstake | 14 |
Proof verification happens on-chain in Solidity via hardcoded verification keys.
Encryption
ElGamal in exponential form on the Grumpkin curve. This variant is additively homomorphic, which allows the contract to update recipient balances via point addition without decrypting.
| Encrypt | Enc(m, pk, r) = (r·G, m·G + r·pk) |
| Decrypt | m·G = C − sk·R, then solve discrete log for m |
| Homomorphic addition | Enc(a) + Enc(b) = Enc(a+b) via component-wise point addition |
The trade-off is that decryption requires solving a discrete log on the Grumpkin curve. See discrete-log solver.
Hash Functions
Poseidon2 is the primary hash function, used everywhere inside circuits:
- Balance commitments
- Merkle tree nodes (3-input hash for ternary trees)
- Nullifier derivation:
Poseidon2(blinding, leafIndex, 0) - Stake commitment:
Poseidon2(amount, blinding) - Compliance transfer IDs
- IMT leaf structure:
Poseidon2(value, nextValue, nextIndex)
The sponge construction uses rate 3, capacity 1 (4-element state). The implementation uses Noir's built-in poseidon2_permutation blackbox.
SHA-256 is used only in TypeScript for key derivation.
Keccak-256 is used on-chain (Solidity) and as an option in the proof backend.
Merkle Trees
Both trees are ternary (3 children per node) with depth 16 (~43 million leaves), using Poseidon2 3-input hash for internal nodes.
Stake tree — append-only, stores stake commitments. The contract keeps historical roots, so proofs against older roots remain valid. This allows concurrent stakes without serialization.
Nullifier IMT (Indexed Merkle Tree) — an ordered linked-list Merkle tree. Each leaf stores {value, nextValue, nextIndex}. Insertion requires updating two paths (the existing low-leaf and the new leaf). The contract does not keep historical roots — only the current root is valid. This serializes all unstake operations. See flaw #6.
Nullifier Derivation
nullifier = Poseidon2(blinding, leafIndex, 0)
blinding is a random secret chosen at stake time. leafIndex is the stake tree position. The nullifier is inserted into the IMT on unstake to prevent double-spending.
On-Chain Arithmetic
Grumpkin point operations in Solidity use Jacobian coordinates internally to avoid per-step modular inverses, converting to affine only at the end. Modular inverse uses the modexp precompile (address 0x05) with Fermat's little theorem.
For shield operations, a precomputed table of 2^i · G (i = 0..63) enables scalar multiplication via binary decomposition, avoiding the full double-and-add loop.