Skip to main content
The Nullis circuit is written in Noir and proven with Barretenberg’s UltraHonk backend. It is deliberately small — everything that does not need to be private is left to the contract.

What the circuit proves

In zero knowledge, from a private credential_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 secret never leaves the prover’s machine. The verifier — the Soroban contract — learns only that these statements hold.

The numbers

Measured, not estimated. Circuit size from bb 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

The stack

Soundness

The circuit is tested for soundness — a valid member is accepted, and forgery attempts are rejected:
A non-member secret, a tampered Merkle path, and a wrong nullifier are each rejected. You cannot produce a proof without a genuine witness.
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.