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 = 21888242871839275222246405745257275088548364400416034343698204186575808495617

The 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 input

3 · 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^64

4 · 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 subscribe

Poseidon parameters

Classic circomlib Poseidon over BN254 Fr. Two width variants, computed by hand:

ArityWidth tFull rounds R_FPartial rounds R_PUsed for
2-inputt = 3857Merkle nodes, nullifier
3-inputt = 4856leaf, 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_i

On 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 14659279386437270083870174181804470449226118232359344769161099059579350216344

Take 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)
     = 16728084433781642160513578623962861653988778837989211022411042828625333450201

The 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:

GadgetCount≈ 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

StageWhereObserved
Witnessbrowser Web Worker~120 to 210 ms
Prove (Groth16)browser Web Worker~600 to 880 ms
Verify (local)browser Web Worker~40 ms
End-to-endbrowser, warm~0.8 to 1.1 s
Verify (on chain)native BN254 host callsingle priced host call

The 256-byte proof and four 32-byte public inputs are the only data that leave the investor’s machine.