Protocol
Proofs and circuits

Registration circuit

What the registration proof shows about a new identity commitment, its eleven public values, how it shares the outgoing viewing key with the guardian nodes, and what the register instruction checks around it.

The registration proof is made once per account. It shows that a new identity commitment is built correctly from non-zero keys, has not been registered before, and that its outgoing viewing key has been split correctly into three shares for the guardian nodes.

What it proves

Your identity commitment (sometimes called the user commitment) is a Poseidon fold of eight slot leaves, each a hash of one value and its own blinder; see your identity commitment. The diagram below shows the eight slots.

Slot 0
spending key
Slot 1
nullifier key
Slot 2
outgoing viewing key
Slot 3
proof-authorisation key
Slot 4
platform wallet address
Slot 5
retired, commits to zero
Slot 6
hash of the ML-KEM-768 public key
Slot 7
reserved, commits to zero
each slot hashed with its own blinder, then the eight leaves hashed together
Identity commitment
The identity commitment the registration proof builds.

The circuit takes the four keys, the platform wallet address, the hash of your ML-KEM-768 public key and the eight blinders as private inputs. The chart below shows what it does with them.

flowchart LR
K["Keys and blinders"] --> I["Identity commitment"]
I --> N["Absent from registration tree"]
K --> S["Three key shares"]
S --> R["Sharing root"]
N --> F["Fold of 11 values"]
R --> F

The checks are these.

The keys are non-zero

The spending key, the nullifier key, the outgoing viewing key and the proof-authorisation key each come with a second private input that must be their inverse. An inverse exists only for a non-zero value, so the product being one proves the key is not zero.

The blinders of slots 4 to 7 are non-zero

The same inverse method forces the blinders of slots 4 to 7 to be non-zero. Every leaf has its own blinder so that a party who learns one slot's value and blinder cannot confirm a guess about any other slot. Slots 4 to 7 hold values that are public or zero (the platform wallet address, the key hash and two zeros), so with a zero blinder their leaves could be confirmed by hashing a guess. Slots 0 to 3 hold the keys, which are already forced non-zero.

The identity commitment is built correctly

The circuit hashes each slot's value with its blinder under a domain specific to that slot, then hashes the eight leaves into the identity commitment. Slot 4 binds the platform wallet address, slot 6 binds the hash of the ML-KEM-768 public key, and slots 5 (retired) and 7 (reserved) commit to zero. Because the platform wallet address and the key hash are also public values, the identity commitment is tied to the wallet that signs the registration and to the key the data endorser checks.

The outgoing viewing key is shared two-of-three

The circuit builds a line through the outgoing viewing key with a slope proven non-zero, and a second line through a blinding value, also with a non-zero slope. It evaluates both lines at x = 1, 2 and 3, hashes each pair into a share leaf tied to its x-coordinate, and hashes the three leaves into a sharing root. Any two shares rebuild the key; one alone reveals nothing about it. A zero slope would make every share equal the key itself, so the non-zero check is what keeps a single guardian node from reading it. See secret sharing.

The identity commitment is not yet registered

The registration tree is an indexed tree of every identity commitment the forester has inserted. The circuit proves the new identity commitment falls strictly between a leaf of that tree and the leaf's successor at the referenced root, the same gap argument the nullifier-freshness circuit uses. This stops an identity from being registered twice once the first registration has reached the tree.

The circuit is RegistrationCircuit() and has 11,112 non-linear constraints.

Entrycircuits/zk/registration.circom
InstantiationRegistrationCircuit()
DEDUP_HIGH_ARITY8
DEDUP_HIGH_LEVELS1
DEDUP_LOW_ARITY8
DEDUP_LOW_LEVELS11
IDENTITY_SLOTS8
SHAMIR_THRESHOLD2
SHAMIR_TOTAL_NODES3
Read at build time from circuits/zk/circom-manifest.json.

The eleven public values

The circuit's public input is a Poseidon fold of eleven values, and the register handler rebuilds it from its own sources.

Sharing threshold and node count (slots 0 and 1). The handler reads them from the guardian cluster's account. The circuit fixes them as the constants 2 and 3, so the proof fails unless the cluster in the payload is a two-of-three cluster.

Identity commitment and sharing root (slots 2 and 3). Both come from the payload. The handler requires each to be a canonical field element, below the field modulus, before using it.

Referenced registration-tree root (slot 4). The payload names it by sequence number, and the handler reads it from the registration staging buffer's ring of recent roots.

Platform wallet address (slots 5 and 6). The two halves come from the address of the account that signed as the platform wallet (the code's bound wallet), never from the payload. A proof made for one wallet cannot register another.

Hash of the ML-KEM-768 public key (slots 7 and 8). The two halves come from the payload. The data endorser checks the hash against the key before it signs.

Context digest (slots 9 and 10). The handler computes Keccak-256 over the payload up to the signature and the addresses of the registration's six accounts, the fee payer's first, and folds its two halves.

What the handler checks around it

register runs these checks in order. Nothing is recorded unless all of them pass.

Check the payload and the signers

The identity commitment and the sharing root must be canonical field elements. The fee payer and the platform wallet must both sign.

Resolve the referenced root

The referenced root must be an active root of the registration staging buffer, which the code calls the turnstile. It keeps the last 120 roots of the registration tree, and an older root is refused as stale.

Resolve the cluster and compute the digest

The handler resolves the guardian cluster named in the payload and refuses it unless its threshold and node count are the 2 of 3 the circuit fixes. It then computes the context digest over the payload up to the signature and folds the eleven public values.

Verify the combined signature

The guardian cluster's nodes and the data endorser sign the digest together, and the handler verifies their combined signature. The guardian nodes sign only after each has checked its share of the outgoing viewing key; the data endorser signs only after checking the key hash and the registration memo digest.

Verify the proof

The handler verifies the proof against the fold, under the 2-of-3 registration key compiled into the program.

Stage and append

The identity commitment is staged in the registration staging buffer, which refuses one already staged, and appended to the pool's identity tree. From then on it can send and receive notes.

What it does not prove

A few facts sit outside the circuit.

The platform wallet halves are a real address. The circuit does not range-check them. The handler fills them from a signing account, so they are always a real 32-byte address.

The ML-KEM-768 key behind the hash is the one published. The circuit sees only the hash. The data endorser checks that the key it is given hashes to the value in the payload, and that the registration memo digest matches that key.

An identity commitment staged but not yet in the registration tree is new. The proof's non-membership check covers only identities the forester has inserted. The registration staging buffer refuses a repeat of any identity it still holds.

Each guardian node received the share its leaf commits to. The circuit commits to the shares but cannot deliver them. Each guardian node checks its own share before it signs.

The proof-authorisation key is ever used. It is committed and forced non-zero, and no current circuit uses it.

The sharing root is public and each share leaf is a hiding commitment, so the proof reveals nothing about the outgoing viewing key.

Last updated

On this page