> For AI agents: the documentation index is at https://docs.umbraprivacy.com/llms.txt, and every page is available as Markdown at its URL with .md appended.

# Registration circuit (/docs/proofs/registration-circuit)

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](https://docs.umbraprivacy.com/docs/account/identity). 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.

```mermaid
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 registration circuit builds the identity commitment, proves it is new, and shares the outgoing viewing key.*

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](https://docs.umbraprivacy.com/docs/cryptography/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](https://docs.umbraprivacy.com/docs/proofs/nullifier-freshness) 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.

| Fact               | Value                             |
| ------------------ | --------------------------------- |
| Entry              | `circuits/zk/registration.circom` |
| Instantiation      | `RegistrationCircuit()`           |
| DEDUP_HIGH_ARITY   | `8`                               |
| DEDUP_HIGH_LEVELS  | `1`                               |
| DEDUP_LOW_ARITY    | `8`                               |
| DEDUP_LOW_LEVELS   | `11`                              |
| IDENTITY_SLOTS     | `8`                               |
| SHAMIR_THRESHOLD   | `2`                               |
| SHAMIR_TOTAL_NODES | `3`                               |

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.

## Related

- [Register](https://docs.umbraprivacy.com/docs/operations/register): The whole registration, step by step.
- [Your identity commitment](https://docs.umbraprivacy.com/docs/account/identity): The eight slots and what each binds.
- [Secret sharing](https://docs.umbraprivacy.com/docs/cryptography/secret-sharing): The two-of-three split and its commitments.
- [The batch append circuit](https://docs.umbraprivacy.com/docs/proofs/batch-append): How registered commitments reach the registration tree.


Last updated 12 October 2026
