[ DOCUMENTATION_MENU ]
[ PROTOCOL ]

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

  1. The buyer's tab verifies the proof against the verification key at /zk/verification_key.json and checks chain(C) == cHash before showing the lock button.
  2. The contract verifies it again in open. DeliveryVerifier is the Groth16 verifier generated by snarkjs for this circuit; an order with an invalid proof reverts with BadProof and nothing moves.
  3. The claim checks Poseidon(k) == keyHash on-chain with a circomlib-compatible Poseidon library, then pays the seller and writes k into the order.
  4. The buyer decrypts C with the revealed k and checks chain(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.