Full Proof Flow with ZkProgram
ZkProgram
is the culmination of all the provable types, functions and constraint system tools in o1js. This is the
building block that let's you actually prove and verify execution of specific code. You can see the api reference
for full type information, but two features that the ZkProgram
gives you are access to the methods you define on
the system which generate a proof, and access to the verify
method that clients can use to verify the proof.
Simple ZkProgram Example
Every ZkProgram
has a name, and a set of public input and output types. These types are shared for every method in
the system, so they can all generate the same kind of proof.
Methods in the ZkProgram
can have custom private inputs and auxiliary output. Private inputs are
where the magic happens. These allow you to pass in private data that you don't want to share with the verifier, and
use it in the proof. Auxiliary output is more often used as a developer utility. It is not available to the verifier,
but it does provide a return value to the prover. This can be used for debugging, or tracking some metadata that's not
available in the public output.
const SimpleZkProgram = ZkProgram({
name: "SimpleZkProgram",
publicInput: Field,
publicOutput: Field,
methods: {
add: {
privateInputs: [Field],
method: async (publicInput: Field, privateInput: Field) => {
return { publicOutput: publicInput.add(privateInput) };
},
},
hash: {
privateInputs: [Field, Field],
method: async (
publicInput: Field,
privateInput1: Field,
privateInput2: Field
) => {
const publicOutput = Poseidon.hash([
publicInput,
privateInput1,
privateInput2,
]);
return { publicOutput };
},
},
},
});
// usage
await SimpleZkProgram.compile(); // Build the constraint system
// Support for different types of inputs on the same type of proof
const proof1 = await SimpleZkProgram.add(Field(10), Field(20));
const proof2 = await SimpleZkProgram.hash(Field(10), Field(20), Field(30));
SimpleZkProgram.verify(proof1.proof);
SimpleZkProgram.verify(proof2.proof);
ZkProgram with complex types
Most real world use cases for ZkProgram
will involve complex data structures. This is where you see the tangible
benefit of the o1js provable type APIs. You can use any provable type as input into a ZkProgram
, so with tools like
Struct, and IndexedMerkleMap,
you can model realistic data structures and easily use them as inputs in your proofs.
const MAX_CANDIDATES = 8;
class Candidate extends Struct({
publicKey: PublicKey,
votes: UInt32,
}) {}
class Election extends Struct({
candidates: Provable.Array(Candidate, MAX_CANDIDATES),
totalVotes: UInt32,
}) {
/**
* Clone method to create a deep copy of the Election instance.
* This is useful to for avoiding side effects in provable code.
*/
clone() {
return new Election({
candidates: this.candidates.map(
(candidate) => new Candidate(candidate)
),
totalVotes: UInt32.from(this.totalVotes),
});
}
}
const VoteProgram = ZkProgram({
name: "VoteProgram",
publicInput: Election,
publicOutput: Election,
methods: {
vote: {
privateInputs: [PublicKey],
method: async (election: Election, candidateKey: PublicKey) => {
const electionClone = election.clone();
for (let i = 0; i < MAX_CANDIDATES; i++) {
const candidate = election.candidates[i];
const newVotes = Provable.if(
candidate.publicKey.equals(candidateKey),
candidate.votes.add(1),
candidate.votes
);
electionClone.candidates[i].votes = newVotes;
}
electionClone.totalVotes = electionClone.totalVotes.add(1);
return {
publicOutput: electionClone,
};
},
},
},
});
// usage
const candidates = Array.from({ length: MAX_CANDIDATES }, (_, i) => {
return new Candidate({
publicKey: PrivateKey.random().toPublicKey(),
votes: UInt32.from(0),
});
});
const election = new Election({
candidates,
totalVotes: UInt32.from(0),
});
await VoteProgram.compile();
const voteProof = await VoteProgram.vote(election, candidates[4].publicKey);
VoteProgram.verify(voteProof.proof);