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 |
| Reveal | 12 |
| Shielded Deposit | 18 |
| Merge | 18 |
| Shielded Withdrawal | 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
- Note commitments (Shielded Layer)
- Nullifier derivation
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.
Note Commitment
A note commitment binds an encrypted amount to the holder's public key:
commitment = Poseidon2(R_x, R_y, S_x, S_y, blinding, PK_x, PK_y)
Where:
(R, S)is the ElGamal ciphertextEnc(amount, PK, r)— the amount encrypted to the holder's public keyblindingis a random secret chosen at deposit time(PK_x, PK_y)is the holder's Grumpkin public key
The PK binding means the note is not a bearer instrument — only the holder who knows the corresponding secret key SK can withdraw. The encrypted balance means intermediaries (e.g., the merge service) handle ciphertexts opaquely without learning plaintext amounts.
Nullifier Mapping
Nullifiers are stored in a flat on-chain mapping:
mapping(bytes32 => bool) public nullifiers;
The contract checks nullifiers[n] == false on each spend and sets nullifiers[n] = true. This is an O(1) lookup — no tree witnesses and no circuit involvement for non-membership proofs. Nullifiers are published on-chain and are publicly visible.
Nullifier Derivation
nullifier = Poseidon2(blinding, leafIndex)
blinding is the random secret from the note commitment. leafIndex is the position of the note commitment in the commitment tree. Anyone who knows both values — including the merge service — can compute the nullifier. This is safe because the nullifier is only a double-spend prevention mechanism: the merge circuit enforces that merged notes can only be consolidated to the identical PK, and only the SK holder can perform a Shielded Withdrawal. Security rests on the circuit constraints, not on nullifier secrecy.
The nullifier is deterministic — the same note always produces the same nullifier, preventing double-spending.
Burn BPK
Administrative and compliance burns send funds to an unspendable burn address. The burn BPK is derived using RFC 9380 hash-to-curve, which maps an arbitrary input to a Grumpkin point with no known discrete log. Since no entity knows the corresponding secret key, funds sent to the burn BPK can never be spent.
burnBPK = hash_to_curve("burn", domain_separator)
The transfer and withdrawal circuits validate BPK ownership via scalar_mul_base(SK) == BPK. Because the burn BPK has no known SK, this check can never be satisfied — the funds are provably irrecoverable.
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.