What the circuit proves
In zero knowledge, from a privatecredential_secret and a Merkle witness:
Possession
The prover holds a
credential_secret whose commitment is Poseidon(credential_secret).Membership
That commitment is a leaf in the policy’s
approved_root — a depth-8 Merkle tree.Nullifier
The nullifier is derived correctly:
Poseidon(credential_secret, policy_id, app_domain, action_id).Binding
The proof is bound to a specific
context_hash, tying it to one exact action.The numbers
Measured, not estimated. Circuit size frombb gates; timings on Apple Silicon.
1,540 gates
UltraHonk circuit size · 73 ACIR opcodes
~0.27 s
bb prove — client-side, off-chain~0.6 s
nargo execute (witness generation)Artifacts
| Artifact | Size |
|---|---|
| Proof | 14,592 bytes (456 field elements) |
| Public inputs | 160 bytes (5 field elements) |
| Verification key | 1,760 bytes |
| Contract wasm (real verifier) | 44,957 bytes |
The stack
Soundness
The circuit is tested for soundness — a valid member is accepted, and forgery attempts are rejected:On-chain, a tampered proof and a tampered public input are also rejected — verified by
cargo test -p nullis-contract, which runs a real UltraHonk proof through the deployed verifier and confirms tampering fails.Why Noir / UltraHonk
Nullis committed to Noir / UltraHonk at Phase 0 and never thrashed the stack. Circom/Groth16 was the fallback only on a hard UltraHonk blocker — which never materialized. Stack-thrashing is the real time-sink in ZK projects; committing early and once was a deliberate engineering decision.Next: unlinkability
How one credential stays unlinkable across many apps.