zkPI: zero-knowledge payment instruction
A proof-carrying instruction. A venue's decision, provable without the inputs that produced it. The settlement layer learns that some enrolled entity holds an instruction whose amount and price lie in declared ranges, whose price matches what a quorum signed, whose deadline has not passed and whose nullifier is unseen. It learns nothing else.
source: zkpi/ZKPI_WIRE.md, zkpi/README.md, defmi/POSITION.md §7 · gadget, instruction and proof crates in the zkpi repository
What crosses and what does not
flowchart LR
subgraph known["what the quorum computes over"]
direction TB
R["the request
asset, size, side"]
P["each maker's
pricing policy"]
W["the winner
and its price"]
end
subgraph published["what is published"]
direction TB
C["committed
instruction"]
N["nullifier"]
D["deadline"]
S["quorum
signature"]
end
subgraph venue["what a settlement venue checks"]
direction TB
V["in range
unspent
in time
signed by the quorum"]
end
R --> C
P --> C
W --> C
C --> V
N --> V
D --> V
S --> V
known -. "never crosses" .-> venue
classDef secret stroke:#f2b880,stroke-width:2px
classDef mech stroke:#9bb0ff,stroke-width:2px
classDef ok stroke:#5ee0c1,stroke-width:2px
class R,P,W secret
class C,N,D,S mech
class V ok
The instruction exists because publicly auditable MPC ends when the output is opened. A quote that is opened and then settled in the clear has leaked everything the computation protected: the asset, the size, the counterparties and, by difference, the policy. zkPI makes the payment instruction itself a commitment plus a proof, so the audited value and the settled value are the same unopened commitment.
What the instruction proves
- Authorisation. A FROST threshold signature over ristretto255 from a 3-of-7 quorum of the venue's nodes. A verifier holds the group key and nothing about which three signed.
- Bounded amount and price. Two range proofs, each jointly assembled from node-local Shamir contributions, so no single node holds the witness. Version 2 carries these as threshold range proofs; version 1 carried two ordinary Bulletproofs.
- Binding to the audited decision. A SHA-256 digest of the complete public quote proof. The digest reveals neither the packed winner nor the price; the validator receives the full proof beside the instruction and checks it independently.
- Single use. A 32-byte nonce from which the nullifier is derived. Settlement registers the nullifier; a second presentation is refused before any proof is examined.
- Counterparties as handles. Two compressed points. A handle is derived per venue from one seed, so the same firm is an unrelated point at each venue. The ledger derives the four account names from the two signed handles; an instruction and a package whose accounts disagree is rejected.
On the wire, version 2
Big-endian throughout. Every field is fixed-width or length-prefixed, in this order. Nothing is optional: an instruction with a field missing is not a shorter instruction, it is a malformed one.
| field | bytes | what |
|---|---|---|
| magic | 8 | QOMMZKPI |
| version | 2 | currently 2. A verifier that meets a version it does not know stops. It does not guess at a layout, because a misparsed commitment is a valid point. |
| amount commitment | 32 | compressed Ristretto |
| price commitment | 32 | compressed Ristretto |
| asset commitment | 32 | compressed Ristretto |
| payer handle | 32 | compressed Ristretto |
| payee handle | 32 | compressed Ristretto |
| deadline | 8 | seconds since the Unix epoch |
| nonce | 32 | what makes the nullifier unique |
| quote proof digest | 32 | SHA-256 of the complete public quote proof |
| signature | 64 | FROST over Ristretto255 |
| amount proof length | 4 | |
| amount range proof | n | jointly assembled threshold range proof |
| price proof length | 4 | |
| price range proof | n | jointly assembled threshold range proof |
The fixed product vector is 9,726 bytes at 16-bit quantity and 32-bit price bounds. The version-1 compatibility vector is 1,572 bytes. The gap is the price of threshold assembly and it is stated rather than presenting the smaller legacy wire as the product cost.
What is deliberately absent
No compression, no self-describing container, no forward compatibility. Each is a way for two implementations to disagree about what they read, and the table above is a day's work to implement from.
Checking an implementation
Vectors live in artifacts/zkpi_vectors/: one accepted instruction per wire version and five damaged forms of each. The accepted vector must decode and re-encode to the same bytes. That is stronger than "it parsed": an implementation that dropped a field and re-derived it would pass the second test and fail the first, and the first is what decides whether two venues settle the same instruction.
zkpi-verify --check-vectors artifacts/zkpi_vectors
zkpi-verify --self-test # issue v2, encode, decode, verify
zkpi-verify --quorum <hex> --now <seconds> < instruction.bin
zkpi-verify < instruction.bin # layout only, and it says so
Exit 0 accepts, 1 rejects, 2 could not be asked. Without a quorum key the tool checks the layout and says that is a parse and not a verification. A tool that printed "ok" for a well-formed unsigned instruction would be worse than none.
Where it runs, and why not elsewhere
The implementation target is the dedicated non-EVM Avalanche L1 described on the DeFMI page. The trust boundary is explicit: the node committee verifies the two threshold range proofs and the complete quote proof before signing; every validator independently checks the 3-of-7 Ed25519 approval, the typed instruction, the complete quote proof, both joint ranges, the taker's price limit, the asset link, the DvP relation, chain and rail domains, deadline, sequence, previous state root and nullifier before applying a transition. Validators do not re-run the private MP-SPDZ transcript or prove the node-local share-to-proof handoff.
The execution-layer comparison, kept as history
Before the dedicated VM existed, the project measured where the version-1 verifier could fit. Its verification is about 1.6 ms of curve arithmetic, dominated by two Bulletproofs.
- EVM bytecode. One ed25519 scalar multiplication measured 302,401 gas. Rejected.
- Subnet-EVM precompile. Feasible: the verifier is Go you write, priced as you choose. The cost is that the curve code becomes yours, and a bug in it is a consensus bug on a chain nobody else is watching.
- Solana. The runtime has ristretto255 syscalls, which is the exact curve here. But a Groth–Kohlweiss verifier evaluates one degree-log₂(n) polynomial per commitment in the scalar field before touching a point, and Solana has no field syscall.
| operation on Solana (Agave 4.2.1) | compute units |
|---|---|
| scalar field: one multiplication | 5,450 |
| scalar field: one addition | 350 |
| Merlin transcript, per 32-byte item | 3,688 |
the same through the sha256 syscall | 132 |
| one base-point scalar mul in the program | ~863,000 |
| one ristretto scalar mul as a syscall (published) | 2,208 |
| one-of-many verifier, crowd of 16 | 398,825 |
| one-of-many verifier, crowd of 32 | 951,503 |
| crowd of 128 (predicted; ceiling is 1,400,000) | ~5,200,000 |
artifact: defmi/artifacts/solana_cu.json · stylus_gas.json
Compute units are counted by the virtual machine rather than timed, so those figures are reproducible to the unit and carry a toolchain label instead of a host label. The bottleneck is the field, not the curve, and that settled the question in favour of a VM where the verifier is native Rust.
Handles: the property that was prose until it was code
The design always said a firm's handle is derived per venue, so that one firm is two unrelated points. The first library offered no way to derive them, so the obvious integration used one name everywhere, and the payment-versus-payment unlinkability measurement showed an observer joining the legs with probability 1.000 at every swap count. zkpi::handles now derives them from one seed, and the test beside it runs the scheme that suggests itself first (one secret scaled by a public per-venue factor) and shows it is publicly linkable. With derived handles the observer falls to 1/k and stays there. Nothing about the cryptography changed between those two rows; only the names did.
What zkPI does not do
- It does not say the price was reasonable or the instrument was right. Those are the quorum's assertions, carried by the signature.
- The current Triptych-based note proof does not publish the sender-known
g^Sas its nullifier. It uses a linking tag that hides the spending secret. Candidate sets, timing and public transaction metadata can still link activity. - It does not prove that a node fed the engine the share it was dealt. That gap is the subject of QOMM's binding document and remains the committee trust boundary.
- Version 1 is a migration format, still decoded and re-encoded when tagged, not the product issuance path.