Proof-of-personhood: status & remaining work
Direction: Noir / Barretenberg UltraHonk, proved on the handset, chosen over Self because the sensitive step never leaves the device (Self offloads it to a TEE) and because it needs no per-circuit trusted setup. Registration proofs are bb v5.0.0 UltraHonk proofs (poseidon2 flavor), verified natively on-chain.
The circuits are ours, not zkPassport's: circuits/ in the mobile repo —
poa_core plus seven lean_poa* variants, one per Document Signer signature
algorithm. What is borrowed from zkPassport is the approach and the proving
library (noir_android), not the circuit.
Done
- On-chain verifier —
zk/ultrahonk: CGo bindings to Barretenberg's own verifier (third_party/barretenberg-go, native lib rebuilt against bb v5.0.0). Verifies real bb 5.0.0 UltraHonk proofs; sound (rejects tampered). Poseidon2 flavor. Seethird_party/barretenberg-go/README.md. - Caretaker wired —
MsgRegister{proof, public_signals, signature_algorithm}→verifyRegistrationProofselects the bb VK fromparams.verifying_keys, callsultrahonk.Verify, checks thedsc_rootbinding, returns the nullifier. Keeper test green against the real proof. - Build infra —
third_party/barretenberg-go/scripts/build-wrapper.sh+checksums.jsonpinned to v5.0.0 for all platforms.
Remaining
-
On-device proving, end to end on real hardware. The code is in place —
PassportProvercallsNoirProverfor a bb v5.0.0 poseidon2 UltraHonk proof of thelean_poacircuit and splits it into the chain's(proof, public_signals)form. What has not been done is a real passport, scanned on a real handset, registering against a real node. Note the version lockstep the file warns about: noir_android's bb must match the chain's v5.0.0 lib or large-circuit proofs are rejected.This is the only open item here. The validator native libs are not one:
lib/carriesdarwin_arm64for dev, and the Linux libs are built per architecture on native runners byrelease.yml, attached to releases byverifier-libs.yml, and built in-image by the Dockerfile.
Settled — do not reopen
-
The public-input schema is pinned and correct. The positions are governance parameters, read in
x/personhood/keeper/registration.goasparams.NullifierIndex,params.DscKeyIndexandparams.CurrentDateIndex, and genesis seeds them 1, 2 and 0.That matches the circuits.
main.nrtakes one public input,current_date, and returns(nullifier, dsc_key), so Barretenberg's public array is[current_date, nullifier, dsc_key]. All seven variants — P-256, P-384, the three brainpools, RSA-2048 and RSA-4096 — have the identical public surface, which is why one set of indices serves every algorithm.Confirmed against a real proof rather than by reading the source:
x/personhood/keeper/testdata/lean_poa/public_inputsholds exactly three field elements —250101(a YYMMDD date), thenexpected_nullifier, thenexpected_dsc_key. The phone agrees:PassportProver.NUM_PUBLIC_INPUTS = 3, split in that order.An earlier version of this file said these were placeholders and that getting them wrong would invalidate every registration. Neither is true any more, and the claim outlived the fix long enough to be repeated as a launch blocker.
-
The verifying keys are seeded, and they are the real ones. Seven distinct keys ship in
networks/genesis/verifying-keys/, one per algorithm. Thelean_poakey is byte-identical to the one the keeper test verifies a real bb proof against, so these arebb write_vkoutput from the actual circuits, not demo keys. -
dsc_rootis gone. It pinned a certificate-registry Merkle root back when the registry was off-chain; the field is nowreservedinproto/earth/personhood/v1/params.proto. A registration carries the Document Signer certificate itself, checked against the CSCA trust store — 539 certificates, seeded in genesis fromcsca/.