Mathematics
Mathematics
In one sentence: you prove you are on the issuer’s approved list and that your amount fits your limit, without revealing which entry is yours. This page starts in plain words, then descends into the exact formulas for readers who want them.
The four claims, in words
The proof makes four promises at once, and reveals nothing else about you:
- You are on the list. Your private entry (a hash of your id, cap, and secret) is one of the leaves the issuer approved.
- Your entry is under the published root. A short path of sibling hashes ties your leaf to the one public fingerprint, the root, without showing the list.
- Your amount fits your cap. The amount you subscribe is not larger than your private limit.
- You have not subscribed twice this round. A one-time tag (the nullifier) is derived from your secret and the round, so a replay is rejected without linking you across rounds.
Only four values become public: [merkle_root, amount, nullifier, epoch]. Everything below is the precise version of the four claims above.
The field
The scalar field modulus, where all witness and public values live:
BN254 Fr modulusr = 21888242871839275222246405745257275088548364400416034343698204186575808495617The statement
1 · Leaf commitment
An accredited investor is a leaf in the issuer’s tree. The leaf binds identity, cap, and a secret:
leaf = Poseidon3(investor_id, cap, investor_secret)2 · Merkle membership
The leaf hashes up a depth-3 path to the committed root. At each level, the path bit b_i ∈ {0,1} orders the pair before hashing:
h_0 = leaf
h_{i+1} = b_i = 0 ? Poseidon2(h_i, sibling_i) : Poseidon2(sibling_i, h_i)
merkle_root = h_3 // public input3 · Range relation
The public amount must not exceed the private cap, and is constrained to 64 bits (7-decimal base units):
amount <= cap and 0 <= amount < 2^644 · Nullifier and commitment
The nullifier binds one subscription per (secret, epoch); the commitment records the subscription without re-revealing the secret:
nullifier = Poseidon2(investor_secret, epoch) // public input
commitment = Poseidon3(nullifier, amount, epoch) // returned by subscribePoseidon parameters
Classic circomlib Poseidon over BN254 Fr. Two width variants, computed by hand:
| Arity | Width t | Full rounds R_F | Partial rounds R_P | Used for |
|---|---|---|---|---|
| 2-input | t = 3 | 8 | 57 | Merkle nodes, nullifier |
| 3-input | t = 4 | 8 | 56 | leaf, commitment |
Groth16 verification
With proof (A, B, C), verifying key (α, β, γ, δ, IC), and public inputs x_1..x_4, the verifier checks one pairing-product equation:
e(A, B) = e(α, β) · e(L, γ) · e(C, δ)
where L = IC_0 + Σ_{i=1..4} x_i · IC_iOn chain this is a single native BN254 pairing host call (CAP-0074). The proof is 256 bytes; the verifying key carries IC of length 5 (one base point plus one per public signal).
A worked 8-leaf example
These are the real numbers from circuits/fixtures/tree.json. The eight leaves:
leaves (Fr)L0 16557954371118903523369896665038481834779587051122472902175676474941900806891
L1 8028477191701634491022509627331333641619373339326806357258960432799413535311
L2 12893205181829035144918275392963016566095938447231477489571980199106169905498
L3 11394988421783667103737052804725122195604032778315219341263051844605992717555
L4 2389403069834309648177430293075600320606710489829300862078302471874876606594
L5 1019621368353778133539355515541377547876040156061198940754797567368030708243
L6 17378101785522100086158976512526383480064960805668474857498890613059552560570
L7 14659279386437270083870174181804470449226118232359344769161099059579350216344Take leaf index 2 (path indices [0, 1, 0]). The path hashes to the root in three Poseidon2 steps:
membership trace, leaf index 2h0 = L2 = 12893205181829035144918275392963016566095938447231477489571980199106169905498
# level 0, b=0, sibling = L3
h1 = Poseidon2(L2, L3)
= 12890551834249873553577838208612920974524018745806333979994876329805909211689
# level 1, b=1, sibling = node[1,0]
h2 = Poseidon2(15433287407569847909368511689487787902083850118714026812950920625613019578967, h1)
= 2494936514636011527722204016798649733204020281115724428000404920469392127035
# level 2, b=0, sibling = node[2,1]
root = Poseidon2(h2, 4876111224302462558615753393385604022409636210284542432896015278460659644565)
= 16728084433781642160513578623962861653988778837989211022411042828625333450201The root
16728084433781642160513578623962861653988778837989211022411042828625333450201 is exactly the merkle_root public input the demo proof on this site produces. You can confirm it on the Prove page.
Constraint budget
The circuit is dominated by Poseidon permutations. Approximate R1CS constraint contributions per gadget:
| Gadget | Count | ≈ constraints |
|---|---|---|
| Poseidon3 (leaf) | 1 | ~245 |
| Poseidon2 (Merkle path) | 3 | ~660 |
| Poseidon2 (nullifier) | 1 | ~220 |
| Poseidon3 (commitment) | 1 | ~245 |
| Range / bit decomposition (amount ≤ cap, 64-bit) | 1 | ~80 |
| Total | ~1,350 |
Performance
| Stage | Where | Observed |
|---|---|---|
| Witness | browser Web Worker | ~120 to 210 ms |
| Prove (Groth16) | browser Web Worker | ~600 to 880 ms |
| Verify (local) | browser Web Worker | ~40 ms |
| End-to-end | browser, warm | ~0.8 to 1.1 s |
| Verify (on chain) | native BN254 host call | single priced host call |
The 256-byte proof and four 32-byte public inputs are the only data that leave the investor’s machine.