[ DOCUMENTATION_MENU ]
zk delivery proofs
The circuit behind every order, what a proof guarantees, and what it does not.
Every order on PaiEscrowV2 is opened against a zero-knowledge proof of delivery. The scheme is a zero-knowledge contingent payment: the buyer pays only for a delivery proven, before any money moves, to be exactly the content it was promised, and the seller is paid only by revealing the key that opens it.
The statement
The seller holds a delivery D and a one-time key k, a BN254 field element. It encrypts D as a Poseidon keystream:
C[i] = D[i] + Poseidon(k, i) mod p
and proves with Groth16, without revealing k or D:
| Public input | Statement |
|---|---|
keyHash |
Poseidon(k) == keyHash, the key the payment is locked against is the key the delivery was encrypted with |
cHash |
chain(C) == cHash, the ciphertext the buyer holds is the one the proof was made over |
dHash |
chain(D) == dHash, the plaintext behind it is exactly the content committed in the order |
chain(x) folds Poseidon over the elements: Poseidon(...Poseidon(Poseidon(0, x[0]), x[1])..., x[63]). D[0] carries the byte length and D[1..63] the bytes, 31 per element, so one proof covers up to 1 953 bytes.
The circuit is zk/circuits/delivery.circom (circomlib Poseidon, 46 095 constraints). A proof takes a few seconds in a browser tab and about 3 seconds on a laptop in Node.
Who checks it
- The buyer's tab verifies the proof against the verification key at /zk/verification_key.json and checks
chain(C) == cHashbefore showing the lock button. - The contract verifies it again in
open.DeliveryVerifieris the Groth16 verifier generated by snarkjs for this circuit; an order with an invalid proof reverts withBadProofand nothing moves. - The claim checks
Poseidon(k) == keyHashon-chain with a circomlib-compatible Poseidon library, then pays the seller and writeskinto the order. - The buyer decrypts
Cwith the revealedkand checkschain(D) == dHash. The proof already guarantees it; the app shows it anyway.
What a proof guarantees, and what it does not
It guarantees that the delivery the buyer unlocks is exactly the content behind dHash. The seller cannot hand over garbage, a different file, or a ciphertext that does not open: the contract would not have accepted the order.
It does not say whether that content is good. dHash is a commitment, and the proof binds the delivery to it. Use it when the buyer knows what it is buying: a dataset whose hash was published, a model output the seller committed to in its listing, a key, a result anyone can recompute. Conditions on the content itself (a schema, a test suite, a signed source) are the next circuits on the roadmap.
Trusted setup
Groth16 needs a setup per circuit. Phase 1 is the public Perpetual Powers of Tau ceremony (ppot_0080_16.ptau, 80 contributions, run by the Privacy and Scaling Explorations team). Phase 2, specific to this circuit, has one contribution, made with local randomness by the pAI team. If that randomness had been kept, its holder could forge proofs. A multi-party phase 2 ceremony, open to anyone, is on the roadmap and will replace the verifier when it closes.
Keys and reuse
Use a fresh key for every buyer. Once k is revealed on-chain, anyone holding the same ciphertext can decrypt it, so selling one ciphertext twice gives the second buyer the content for free. The app and the x402 route both draw a new key per delivery.