o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
1,220 lines (1,101 loc) • 40.4 kB
text/typescript
import {
Base64ProofString,
Base64VerificationKeyString,
Gate,
Pickles,
Snarky,
initializeBindings,
withThreadPool,
} from '../../bindings.js';
import { setSrsCache, unsetSrsCache } from '../../bindings/crypto/bindings/srs.js';
import { prefixes } from '../../bindings/crypto/constants.js';
import { prefixToField } from '../../bindings/lib/binable.js';
import { EmptyUndefined, EmptyVoid } from '../../bindings/lib/generic.js';
import { From, InferValue } from '../../bindings/lib/provable-generic.js';
import { MlArray, MlBool, MlPair, MlResult } from '../ml/base.js';
import { MlFieldArray, MlFieldConstArray } from '../ml/fields.js';
import { FieldConst, FieldVar } from '../provable/core/fieldvar.js';
import { ConstraintSystemSummary, snarkContext } from '../provable/core/provable-context.js';
import { hashConstant } from '../provable/crypto/poseidon.js';
import { Provable } from '../provable/provable.js';
import { InferProvableType } from '../provable/types/provable-derivers.js';
import { ProvableType, ToProvable } from '../provable/types/provable-intf.js';
import { FlexibleProvable, InferProvable, ProvablePureExtended } from '../provable/types/struct.js';
import { emptyWitness } from '../provable/types/util.js';
import { Field } from '../provable/wrapped.js';
import { mapObject, mapToObject, zip } from '../util/arrays.js';
import { assert, prettifyStacktracePromise } from '../util/errors.js';
import { Get, Subclass, Tuple } from '../util/types.js';
import { Cache, readCache, writeCache } from './cache.js';
import { featureFlagsFromGates, featureFlagsToMlOption } from './feature-flags.js';
import {
DynamicProof,
Proof,
ProofBase,
ProofClass,
ProofValue,
dummyProof,
extractProofTypes,
extractProofs,
} from './proof.js';
import { decodeProverKey, encodeProverKey, parseHeader } from './prover-keys.js';
import { VerificationKey } from './verification-key.js';
import { DeclaredProof, ZkProgramContext } from './zkprogram-context.js';
// public API
export { Empty, JsonProof, Method, SelfProof, Undefined, Void, ZkProgram, verify };
// internal API
export {
CompiledTag,
MethodInterface,
MethodReturnType,
PrivateInput,
Proof,
Prover,
RegularProver,
TupleToInstances,
analyzeMethod,
compileProgram,
computeMaxProofsVerified,
dummyBase64Proof,
inCircuitVkHash,
picklesRuleFromFunction,
sortMethodArguments,
};
type Undefined = undefined;
const Undefined: ProvablePureExtended<undefined, undefined, null> = EmptyUndefined<Field>();
type Empty = Undefined;
const Empty = Undefined;
type Void = undefined;
const Void: ProvablePureExtended<void, void, null> = EmptyVoid<Field>();
type MethodAnalysis = ConstraintSystemSummary & {
proofs: ProofClass[];
};
function createProgramState() {
let methodCache: Map<string, unknown> = new Map();
return {
setNonPureOutput(value: any[]) {
methodCache.set('__nonPureOutput__', value);
},
getNonPureOutput(): any[] {
let entry = methodCache.get('__nonPureOutput__');
if (entry === undefined) return [];
return entry as any[];
},
setAuxiliaryOutput(value: unknown, methodName: string) {
methodCache.set(methodName, value);
},
getAuxiliaryOutput(methodName: string): unknown {
let entry = methodCache.get(methodName);
if (entry === undefined) throw Error(`Auxiliary value for method ${methodName} not defined`);
return entry;
},
reset(key: string) {
methodCache.delete(key);
},
};
}
/**
* Initializes Pickles bindings, serializes the input proof and verification key for use in OCaml, then calls into the Pickles verify function and returns the result.
*
* @note This function is meant to be called in JavaScript, not for use in a circuit. The verification key data and hash are not confirmed to match.
* @param proof Either a `Proof` instance or a serialized JSON proof
* @param verificationKey Either a base64 serialized verification key or a `VerificationKey` instance which will be base64 serialized for use in the bindings.
* @returns A promise that resolves to a boolean indicating whether the proof is valid.
*/
async function verify(
proof: ProofBase<any, any> | JsonProof,
verificationKey: Base64VerificationKeyString | VerificationKey
) {
await initializeBindings();
let picklesProof: Pickles.Proof;
let statement: Pickles.Statement<FieldConst>;
if (typeof proof.proof === 'string') {
// json proof
[, picklesProof] = Pickles.proofOfBase64(proof.proof, proof.maxProofsVerified);
let input = MlFieldConstArray.to((proof as JsonProof).publicInput.map(Field));
let output = MlFieldConstArray.to((proof as JsonProof).publicOutput.map(Field));
statement = MlPair(input, output);
} else {
// proof class
picklesProof = proof.proof;
let fields = (proof as ProofBase).publicFields();
let input = MlFieldConstArray.to(fields.input);
let output = MlFieldConstArray.to(fields.output);
statement = MlPair(input, output);
}
let vk = typeof verificationKey === 'string' ? verificationKey : verificationKey.data;
return prettifyStacktracePromise(
withThreadPool(() => Pickles.verify(statement, picklesProof, vk))
);
}
/**
* Serializable representation of a Pickles proof, useful for caching compiled proofs.
*/
type JsonProof = {
/** Array of string, where each string is a `Field` in the publicInput of this proof */
publicInput: string[];
/** Array of string, where each string is a `Field` in the publicOutput of this proof */
publicOutput: string[];
maxProofsVerified: 0 | 1 | 2;
proof: Base64ProofString;
};
type CompiledTag = unknown;
let compiledTags = new WeakMap<any, CompiledTag>();
let CompiledTag = {
get(tag: any): CompiledTag | undefined {
return compiledTags.get(tag);
},
store(tag: any, compiledTag: CompiledTag) {
compiledTags.set(tag, compiledTag);
},
};
let sideloadedKeysMap: Record<string, unknown> = {};
let SideloadedTag = {
get(tag: string): unknown | undefined {
return sideloadedKeysMap[tag];
},
store(tag: string, compiledTag: unknown) {
sideloadedKeysMap[tag] = compiledTag;
},
};
type ConfigBaseType = {
publicInput?: ProvableType;
publicOutput?: ProvableType;
methods: {
[I in string]: {
privateInputs: Tuple<PrivateInput>;
auxiliaryOutput?: ProvableType;
};
};
};
type InferMethodSignatures<Config extends ConfigBaseType> = Config['methods'];
type InferPrivateInput<Config extends ConfigBaseType> = {
[I in keyof Config['methods']]: Config['methods'][I]['privateInputs'];
};
type InferAuxiliaryOutputs<Config extends ConfigBaseType> = {
[I in keyof InferMethodSignatures<Config>]: Get<
InferMethodSignatures<Config>[I],
'auxiliaryOutput'
>;
};
type InferMethodType<Config extends ConfigBaseType> = {
[I in keyof Config['methods']]: Method<
InferProvableOrUndefined<Get<Config, 'publicInput'>>,
InferProvableOrVoid<Get<Config, 'publicOutput'>>,
Config['methods'][I]
>;
};
/**
* Wraps config + provable code into a program capable of producing {@link Proof}s.
*
* @example
* ```ts
* const ExampleProgram = ZkProgram({
* name: 'ExampleProgram',
* publicOutput: Int64,
* methods: {
* // Prove that I know 2 numbers less than 100 each, whose product is greater than 1000
* provableMultiply: {
* privateInputs: [Int64, Int64],
* method: async (n1: Int64, n2: Int64) => {
* n1.assertLessThan(100);
* n2.assertLessThan(100);
* const publicOutput = n1.mul(n2);
* publicOutput.assertGreaterThan(1000);
* return { publicOutput: n1.mul(n2) }
* }
* }
* }
* });
* ```
*
* @param config The configuration of the program, describing the type of the public input and public output, as well as defining the methods which can be executed provably.
* @returns an object that can be used to compile, prove, and verify the program.
*/
function ZkProgram<
Config extends ConfigBaseType,
_ extends unknown = unknown, // weird hack that makes methods infer correctly when their inputs are not annotated
>(
config: Config & {
name: string;
methods: {
[I in keyof Config['methods']]: InferMethodType<Config>[I];
};
overrideWrapDomain?: 0 | 1 | 2;
numChunks?: number;
}
): {
name: string;
maxProofsVerified(): Promise<0 | 1 | 2>;
compile: (options?: {
cache?: Cache;
forceRecompile?: boolean;
proofsEnabled?: boolean;
withRuntimeTables?: boolean;
numChunks?: number;
lazyMode?: boolean;
}) => Promise<{
verificationKey: { data: string; hash: Field };
}>;
verify: (
proof: Proof<
InferProvableOrUndefined<Get<Config, 'publicInput'>>,
InferProvableOrVoid<Get<Config, 'publicOutput'>>
>
) => Promise<boolean>;
digest: () => Promise<string>;
/**
* Analyze the constraint system created by each method in the program.
* Every method is executed in a circuit, and the constraints are analyzed.
*
* @returns A summary of this ZkProgram, keyed by the method name, with a value of the {@link MethodAnalysis} for that method
*/
analyzeMethods: () => Promise<{
[I in keyof Config['methods']]: MethodAnalysis;
}>;
/**
* Analyze the constraint system created by a single method in the program without analyzing any other methods and executing them.
*
* @returns A summary of this method, with a value of the {@link MethodAnalysis} for that method
*/
analyzeSingleMethod<K extends keyof Config['methods']>(methodName: K): Promise<MethodAnalysis>;
publicInputType: ProvableOrUndefined<Get<Config, 'publicInput'>>;
publicOutputType: ProvableOrVoid<Get<Config, 'publicOutput'>>;
privateInputTypes: InferPrivateInput<Config>;
auxiliaryOutputTypes: InferAuxiliaryOutputs<Config>;
rawMethods: {
[I in keyof Config['methods']]: InferMethodType<Config>[I]['method'];
};
Proof: typeof Proof<
InferProvableOrUndefined<Get<Config, 'publicInput'>>,
InferProvableOrVoid<Get<Config, 'publicOutput'>>
>;
proofsEnabled: boolean;
setProofsEnabled(proofsEnabled: boolean): void;
} & {
[I in keyof Config['methods']]: Prover<
InferProvableOrUndefined<Get<Config, 'publicInput'>>,
ProvableOrUndefined<Get<Config, 'publicInput'>>,
InferProvableOrVoid<Get<Config, 'publicOutput'>>,
InferPrivateInput<Config>[I],
InferProvableOrUndefined<InferAuxiliaryOutputs<Config>[I]>
>;
} {
type PublicInputType = ProvableOrUndefined<Get<Config, 'publicInput'>>;
type PublicInput = InferProvableOrUndefined<Get<Config, 'publicInput'>>;
type PublicOutput = InferProvableOrVoid<Get<Config, 'publicOutput'>>;
// derived types for convenience
type Methods = InferMethodSignatures<Config>;
type PrivateInputs = InferPrivateInput<Config>;
type AuxiliaryOutputs = InferAuxiliaryOutputs<Config>;
let doProving = true;
let methods = config.methods;
let publicInputType: Provable<any> = ProvableType.get(config.publicInput ?? Undefined);
let hasPublicInput = publicInputType !== Undefined && publicInputType !== Void;
let publicOutputType: Provable<any> = ProvableType.get(config.publicOutput ?? Void);
let selfTag = { name: config.name };
class SelfProof extends Proof<PublicInput, PublicOutput> {
static publicInputType = publicInputType;
static publicOutputType = publicOutputType;
static tag = () => selfTag;
}
type MethodKey = keyof Config['methods'];
// TODO remove sort()! Object.keys() has a deterministic order
let methodKeys: MethodKey[] = Object.keys(methods).sort(); // need to have methods in (any) fixed order
let methodIntfs = methodKeys.map((key) =>
sortMethodArguments(
'program',
key as string,
methods[key].privateInputs,
ProvableType.get(methods[key].auxiliaryOutput) ?? Undefined,
SelfProof
)
);
let methodFunctions = methodKeys.map((key) => methods[key].method);
let privateInputTypes = methodIntfs.map((m) => m.args);
let maxProofsVerified: undefined | 0 | 1 | 2 = undefined;
async function getMaxProofsVerified() {
if (maxProofsVerified !== undefined) return maxProofsVerified;
let methodsMeta = await analyzeMethods();
let proofs = methodKeys.map((k) => methodsMeta[k].proofs.length);
maxProofsVerified = computeMaxProofsVerified(proofs);
return maxProofsVerified;
}
async function analyzeMethods() {
let methodsMeta: Record<string, MethodAnalysis> = {};
for (let i = 0; i < methodIntfs.length; i++) {
let methodEntry = methodIntfs[i];
methodsMeta[methodEntry.methodName] = await analyzeMethod(
publicInputType,
methodEntry,
methodFunctions[i]
);
}
return methodsMeta as {
[I in keyof Methods]: MethodAnalysis;
};
}
async function analyzeSingleMethod<K extends keyof Methods>(
methodName: K
): Promise<MethodAnalysis> {
let methodIntf = methodIntfs[methodKeys.indexOf(methodName)];
let methodImpl = methodFunctions[methodKeys.indexOf(methodName)];
return await analyzeMethod(publicInputType, methodIntf, methodImpl);
}
let compileOutput:
| {
provers: Pickles.Prover[];
maxProofsVerified: 0 | 1 | 2;
verify: (
statement: Pickles.Statement<FieldConst>,
proof: Pickles.Proof
) => Promise<boolean>;
}
| undefined;
const programState = createProgramState();
async function compile({
cache = Cache.FileSystemDefault,
forceRecompile = false,
proofsEnabled = undefined as boolean | undefined,
withRuntimeTables = false,
lazyMode = false,
} = {}) {
doProving = proofsEnabled ?? doProving;
if (doProving) {
let methodsMeta = await analyzeMethods();
let gates = methodKeys.map((k) => methodsMeta[k].gates);
let proofs = methodKeys.map((k) => methodsMeta[k].proofs);
maxProofsVerified = computeMaxProofsVerified(proofs.map((p) => p.length));
let { provers, verify, verificationKey } = await compileProgram({
publicInputType,
publicOutputType,
methodIntfs,
methods: methodFunctions,
gates,
proofs,
proofSystemTag: selfTag,
cache,
forceRecompile,
overrideWrapDomain: config.overrideWrapDomain,
numChunks: config.numChunks,
state: programState,
withRuntimeTables,
lazyMode,
});
compileOutput = { provers, verify, maxProofsVerified };
return { verificationKey };
} else {
return {
verificationKey: VerificationKey.empty(),
};
}
}
// for each of the methods, create a prover function.
// in the first step, these are "regular" in that they always expect the public input as the first argument,
// which is easier to use internally.
type RegularProver_<K extends MethodKey> = RegularProver<
PublicInput,
PublicInputType,
PublicOutput,
PrivateInputs[K],
InferProvableOrUndefined<AuxiliaryOutputs[K]>
>;
function toRegularProver<K extends MethodKey>(key: K, i: number): RegularProver_<K> {
return async function prove_(inputPublicInput, ...inputArgs) {
let publicInput = publicInputType.fromValue(inputPublicInput);
let args = zip(inputArgs, privateInputTypes[i]).map(([arg, type]) =>
ProvableType.get(type).fromValue(arg)
);
if (!doProving) {
// we step into a ZkProgramContext here to match the context nesting
// that would happen if proofs were enabled -- otherwise, proofs declared
// in an inner program could be counted to the outer program
let id = ZkProgramContext.enter();
try {
let { publicOutput, auxiliaryOutput } =
(hasPublicInput
? await (methods[key].method as any)(publicInput, ...args)
: await (methods[key].method as any)(...args)) ?? {};
let proof = await SelfProof.dummy(
publicInput,
publicOutput,
await getMaxProofsVerified()
);
return { proof, auxiliaryOutput };
} finally {
ZkProgramContext.leave(id);
}
}
if (compileOutput === undefined) {
throw Error(
`Cannot prove execution of program.${String(key)}(), no prover found. ` +
`Try calling \`await program.compile()\` first, this will cache provers in the background.\nIf you compiled your zkProgram with proofs disabled (\`proofsEnabled = false\`), you have to compile it with proofs enabled first.`
);
}
let picklesProver = compileOutput.provers[i];
let maxProofsVerified = compileOutput.maxProofsVerified;
let { publicInputFields, publicInputAux } = toFieldAndAuxConsts(publicInputType, publicInput);
let id = snarkContext.enter({
witnesses: args,
inProver: true,
auxInputData: publicInputAux,
});
let result: UnwrapPromise<ReturnType<typeof picklesProver>>;
try {
result = await picklesProver(publicInputFields);
} finally {
snarkContext.leave(id);
}
let auxiliaryType = methodIntfs[i].auxiliaryType;
let auxiliaryOutputExists = auxiliaryType && auxiliaryType.sizeInFields() !== 0;
let auxiliaryOutput;
if (auxiliaryOutputExists) {
auxiliaryOutput = programState.getAuxiliaryOutput(methodIntfs[i].methodName);
programState.reset(methodIntfs[i].methodName);
}
let [publicOutputFields, proof] = MlPair.from(result);
let nonPureOutput = programState.getNonPureOutput();
let publicOutput = fromFieldConsts(publicOutputType, publicOutputFields, nonPureOutput);
programState.reset('__nonPureOutput__');
return {
proof: new SelfProof({
publicInput,
publicOutput,
proof,
maxProofsVerified,
}),
auxiliaryOutput,
};
};
}
let regularProvers = mapToObject(methodKeys, toRegularProver);
// wrap "regular" provers to remove an `undefined` public input argument,
// this matches how the method itself was defined in the case of no public input
type Prover_<K extends MethodKey = MethodKey> = Prover<
PublicInput,
PublicInputType,
PublicOutput,
PrivateInputs[K],
InferProvableOrUndefined<AuxiliaryOutputs[K]>
>;
type Provers = {
[K in MethodKey]: Prover_<K>;
};
let provers: Provers = mapObject(regularProvers, (prover): Prover_ => {
if (publicInputType === Undefined || publicInputType === Void) {
return ((...args: any) => prover(undefined as any, ...args)) as any;
} else {
return prover as any;
}
});
function verify(proof: Proof<PublicInput, PublicOutput>) {
if (!doProving) {
return Promise.resolve(true);
}
if (compileOutput?.verify === undefined) {
throw Error(
`Cannot verify proof, verification key not found. Try calling \`await program.compile()\` first.`
);
}
let statement = MlPair(
toFieldConsts(publicInputType, proof.publicInput),
toFieldConsts(publicOutputType, proof.publicOutput)
);
return compileOutput.verify(statement, proof.proof);
}
async function digest() {
let methodsMeta = await analyzeMethods();
let digests: Field[] = methodKeys.map((k) => Field(BigInt('0x' + methodsMeta[k].digest)));
return hashConstant(digests).toBigInt().toString(16);
}
const program = Object.assign(
selfTag,
{
maxProofsVerified: getMaxProofsVerified,
compile,
verify,
digest,
analyzeMethods,
analyzeSingleMethod,
publicInputType: publicInputType as ProvableOrUndefined<Get<Config, 'publicInput'>>,
publicOutputType: publicOutputType as ProvableOrVoid<Get<Config, 'publicOutput'>>,
privateInputTypes: mapToObject(methodKeys, (_, i) => privateInputTypes[i]) as any,
auxiliaryOutputTypes: Object.fromEntries(
methodKeys.map((key) => [key, methods[key].auxiliaryOutput])
) as any,
rawMethods: Object.fromEntries(methodKeys.map((key) => [key, methods[key].method])) as any,
Proof: SelfProof,
proofsEnabled: doProving,
setProofsEnabled(proofsEnabled: boolean) {
doProving = proofsEnabled;
},
},
provers
);
// Object.assign only shallow-copies, hence we can't use this getter and have to define it explicitly
Object.defineProperty(program, 'proofsEnabled', {
get: () => doProving,
});
return program;
}
type ZkProgram<
Config extends {
publicInput?: ProvableType;
publicOutput?: ProvableType;
methods: {
[I in string]: {
privateInputs: Tuple<PrivateInput>;
auxiliaryOutput?: ProvableType;
};
};
},
> = ReturnType<typeof ZkProgram<Config>>;
/**
* A class representing the type of Proof produced by the {@link ZkProgram} in which it is used.
*
* @example
* ```ts
* const ExampleProgram = ZkProgram({
* name: 'ExampleProgram',
* publicOutput: Field,
* methods: {
* baseCase: {
* privateInputs: [],
* method: async () => {
* return { publicOutput: Field(0) }
* }
* },
* add: {
* privateInputs: [SelfProof, Field],
* // `previous` is the type of proof produced by ExampleProgram
* method: async (previous: SelfProof<undefined, Field>, f: Field) => {
* previous.verify();
* return { publicOutput: previous.publicOutput.add(f) }
* }
* }
* }
* });
* ```
*/
class SelfProof<PublicInput, PublicOutput> extends Proof<PublicInput, PublicOutput> {}
function sortMethodArguments(
programName: string,
methodName: string,
privateInputs: unknown[],
auxiliaryType: Provable<any> | undefined,
selfProof: Subclass<typeof Proof>
): MethodInterface {
// replace SelfProof with the actual selfProof
// TODO this does not handle SelfProof nested in inputs
privateInputs = privateInputs.map((input) => (input === SelfProof ? selfProof : input));
// check if all arguments are provable
let args: ProvableType<unknown>[] = privateInputs.map((input, i) => {
if (isProvable(input)) return input;
throw Error(`Argument ${i + 1} of method ${methodName} is not a provable type: ${input}`);
});
// extract input proofs to count them and for sanity checks
// WARNING: this doesn't include internally declared proofs!
let proofs = args.flatMap(extractProofTypes);
let numberOfProofs = proofs.length;
// don't allow base classes for proofs
proofs.forEach((proof) => {
if (proof === ProofBase || proof === Proof || proof === DynamicProof) {
throw Error(
`You cannot use the \`${proof.name}\` class directly. Instead, define a subclass:\n` +
`class MyProof extends ${proof.name}<PublicInput, PublicOutput> { ... }`
);
}
});
// don't allow more than 2 proofs
if (numberOfProofs > 2) {
throw Error(
`${programName}.${methodName}() has more than two proof arguments, which is not supported.\n` +
`Suggestion: You can merge more than two proofs by merging two at a time in a binary tree.`
);
}
return { methodName, args, auxiliaryType };
}
function isProvable(type: unknown): type is ProvableType<unknown> {
let type_ = ProvableType.get(type);
return (
(typeof type_ === 'function' || typeof type_ === 'object') &&
type_ !== null &&
['toFields', 'fromFields', 'sizeInFields', 'toAuxiliary'].every((s) => s in type_)
);
}
function isDynamicProof(type: Subclass<typeof ProofBase>): type is Subclass<typeof DynamicProof> {
return typeof type === 'function' && type.prototype instanceof DynamicProof;
}
type MethodInterface = {
methodName: string;
args: ProvableType<unknown>[];
returnType?: Provable<any>;
auxiliaryType?: Provable<any>;
};
// reasonable default choice for `overrideWrapDomain`
const maxProofsToWrapDomain = { 0: 0, 1: 1, 2: 1 } as const;
async function compileProgram({
publicInputType,
publicOutputType,
methodIntfs,
methods,
gates,
proofs,
proofSystemTag,
cache,
forceRecompile,
overrideWrapDomain,
numChunks,
state,
withRuntimeTables,
lazyMode,
}: {
publicInputType: Provable<any>;
publicOutputType: Provable<any>;
methodIntfs: MethodInterface[];
methods: ((...args: any) => unknown)[];
gates: Gate[][];
proofs: ProofClass[][];
proofSystemTag: { name: string };
cache: Cache;
forceRecompile: boolean;
overrideWrapDomain?: 0 | 1 | 2;
numChunks?: number;
state?: ReturnType<typeof createProgramState>;
withRuntimeTables?: boolean;
lazyMode?: boolean;
}) {
await initializeBindings();
if (methodIntfs.length === 0)
throw Error(`The Program you are trying to compile has no methods.
Try adding a method to your ZkProgram or SmartContract.
If you are using a SmartContract, make sure you are using the @method decorator.`);
let rules = methodIntfs.map((methodEntry, i) =>
picklesRuleFromFunction(
publicInputType,
publicOutputType,
methods[i],
proofSystemTag,
methodEntry,
gates[i],
proofs[i],
state,
withRuntimeTables
)
);
let maxProofs = computeMaxProofsVerified(proofs.map((p) => p.length));
overrideWrapDomain ??= maxProofsToWrapDomain[maxProofs];
let picklesCache: Pickles.Cache = [
0,
function read_(mlHeader) {
if (forceRecompile) return MlResult.unitError();
let header = parseHeader(proofSystemTag.name, methodIntfs, mlHeader);
let result = readCache(cache, header, (bytes) => decodeProverKey(mlHeader, bytes));
if (result === undefined) return MlResult.unitError();
return MlResult.ok(result);
},
function write_(mlHeader, value) {
if (!cache.canWrite) return MlResult.unitError();
let header = parseHeader(proofSystemTag.name, methodIntfs, mlHeader);
let didWrite = writeCache(cache, header, encodeProverKey(value));
if (!didWrite) return MlResult.unitError();
return MlResult.ok(undefined);
},
MlBool(cache.canWrite),
];
let { verificationKey, provers, verify, tag } = await prettifyStacktracePromise(
withThreadPool(async () => {
let result: ReturnType<typeof Pickles.compile>;
let id = snarkContext.enter({ inCompile: true });
setSrsCache(cache);
try {
result = Pickles.compile(MlArray.to(rules), {
publicInputSize: publicInputType.sizeInFields(),
publicOutputSize: publicOutputType.sizeInFields(),
storable: picklesCache,
overrideWrapDomain,
numChunks: numChunks ?? 1,
lazyMode: lazyMode ?? false,
});
let { getVerificationKey, provers, verify, tag } = result;
CompiledTag.store(proofSystemTag, tag);
let [, data, hash] = await getVerificationKey();
let verificationKey = { data, hash: Field(hash) };
return {
verificationKey,
provers: MlArray.from(provers),
verify,
tag,
};
} finally {
snarkContext.leave(id);
unsetSrsCache();
}
})
);
// wrap provers
let wrappedProvers = provers.map(
(prover): Pickles.Prover =>
async function picklesProver(publicInput: MlFieldConstArray) {
return prettifyStacktracePromise(withThreadPool(() => prover(publicInput)));
}
);
// wrap verify
let wrappedVerify = async function picklesVerify(
statement: Pickles.Statement<FieldConst>,
proof: Pickles.Proof
) {
return prettifyStacktracePromise(withThreadPool(() => verify(statement, proof)));
};
return {
verificationKey,
provers: wrappedProvers,
verify: wrappedVerify,
tag,
};
}
async function analyzeMethod(
publicInputType: Provable<any>,
methodIntf: MethodInterface,
method: (...args: any) => unknown
): Promise<MethodAnalysis> {
let result: ConstraintSystemSummary;
let proofs: ProofClass[];
let id = ZkProgramContext.enter();
try {
result = await Provable.constraintSystem(() => {
let args = methodIntf.args.map(emptyWitness);
args.forEach((value) => extractProofs(value).forEach((proof) => proof.declare()));
let publicInput = emptyWitness(publicInputType);
// note: returning the method result here makes this handle async methods
if (publicInputType === Undefined || publicInputType === Void) return method(...args);
return method(publicInput, ...args);
});
proofs = ZkProgramContext.getDeclaredProofs().map(({ ProofClass }) => ProofClass);
} finally {
ZkProgramContext.leave(id);
}
return { ...result, proofs };
}
function inCircuitVkHash(inCircuitVk: unknown): Field {
const digest = Pickles.sideLoaded.vkDigest(inCircuitVk);
const salt = Snarky.poseidon.update(
MlFieldArray.to([Field(0), Field(0), Field(0)]),
MlFieldArray.to([prefixToField(Field, prefixes.sideLoadedVK)])
);
const newState = Snarky.poseidon.update(salt, digest);
const stateFields = MlFieldArray.from(newState) as [Field, Field, Field];
return stateFields[0];
}
function picklesRuleFromFunction(
publicInputType: Provable<unknown>,
publicOutputType: Provable<unknown>,
func: (...args: unknown[]) => unknown,
proofSystemTag: { name: string },
{ methodName, args, auxiliaryType }: MethodInterface,
gates: Gate[],
verifiedProofs: ProofClass[],
state?: ReturnType<typeof createProgramState>,
withRuntimeTables?: boolean
): Pickles.Rule {
async function main(publicInput: MlFieldArray): ReturnType<Pickles.Rule['main']> {
let { witnesses: argsWithoutPublicInput, inProver, auxInputData } = snarkContext.get();
assert(!(inProver && argsWithoutPublicInput === undefined));
// witness private inputs and declare input proofs
let id = ZkProgramContext.enter();
let finalArgs = [];
for (let i = 0; i < args.length; i++) {
try {
let type = args[i];
let value = Provable.witness(type, () => {
return argsWithoutPublicInput?.[i] ?? ProvableType.synthesize(type);
});
finalArgs[i] = value;
extractProofs(value).forEach((proof) => proof.declare());
} catch (e: any) {
ZkProgramContext.leave(id);
e.message = `Error when witnessing in ${methodName}, argument ${i}: ${e.message}`;
throw e;
}
}
// run the user circuit
let result: { publicOutput?: any; auxiliaryOutput?: any };
let proofs: DeclaredProof[];
try {
if (publicInputType === Undefined || publicInputType === Void) {
result = (await func(...finalArgs)) as any;
} else {
let input = fromFieldVars(publicInputType, publicInput, auxInputData);
result = (await func(input, ...finalArgs)) as any;
}
proofs = ZkProgramContext.getDeclaredProofs();
} finally {
ZkProgramContext.leave(id);
}
if (result?.publicOutput) {
// store the nonPure auxiliary data in program state cache if it exists
let nonPureOutput = publicOutputType.toAuxiliary(result.publicOutput);
state?.setNonPureOutput(nonPureOutput);
}
// now all proofs are declared - check that we got as many as during compile time
assert(
proofs.length === verifiedProofs.length,
`Expected ${verifiedProofs.length} proofs, but got ${proofs.length}`
);
// extract proof statements for Pickles
let previousStatements = proofs.map(({ proofInstance }): Pickles.Statement<FieldVar> => {
let fields = proofInstance.publicFields();
let input = MlFieldArray.to(fields.input);
let output = MlFieldArray.to(fields.output);
return MlPair(input, output);
});
// handle dynamic proofs
proofs.forEach(({ ProofClass, proofInstance }) => {
if (!(proofInstance instanceof DynamicProof)) return;
// Initialize side-loaded verification key
const tag = ProofClass.tag();
const computedTag = SideloadedTag.get(tag.name);
const vk = proofInstance.usedVerificationKey;
if (vk === undefined) {
throw new Error('proof.verify() not called, call it at least once in your circuit');
}
if (Provable.inProver()) {
Pickles.sideLoaded.inProver(computedTag, vk.data);
}
const circuitVk = Pickles.sideLoaded.vkToCircuit(() => vk.data);
// Assert the validity of the auxiliary vk-data by comparing the witnessed and computed hash
const hash = inCircuitVkHash(circuitVk);
Field(hash).assertEquals(vk.hash, 'Provided VerificationKey hash not correct');
Pickles.sideLoaded.inCircuit(computedTag, circuitVk);
});
// if the output is empty, we don't evaluate `toFields(result)` to allow the function to return something else in that case
let hasPublicOutput = publicOutputType.sizeInFields() !== 0;
let publicOutput = hasPublicOutput ? publicOutputType.toFields(result.publicOutput) : [];
if (state !== undefined && auxiliaryType !== undefined && auxiliaryType.sizeInFields() !== 0) {
Provable.asProver(() => {
let { auxiliaryOutput } = result;
assert(
auxiliaryOutput !== undefined,
`${proofSystemTag.name}.${methodName}(): Auxiliary output is undefined even though the method declares it.`
);
state.setAuxiliaryOutput(Provable.toConstant(auxiliaryType, auxiliaryOutput), methodName);
});
}
return {
publicOutput: MlFieldArray.to(publicOutput),
previousStatements: MlArray.to(previousStatements),
previousProofs: MlArray.to(proofs.map((p) => p.proofInstance.proof)),
shouldVerify: MlArray.to(
proofs.map((proof) => proof.proofInstance.shouldVerify.toField().value)
),
};
}
if (verifiedProofs.length > 2) {
throw Error(
`${proofSystemTag.name}.${methodName}() has more than two proof arguments, which is not supported.\n` +
`Suggestion: You can merge more than two proofs by merging two at a time in a binary tree.`
);
}
let proofsToVerify = verifiedProofs.map((Proof) => {
let tag = Proof.tag();
if (tag === proofSystemTag) return { isSelf: true as const };
else if (isDynamicProof(Proof)) {
let computedTag: unknown;
// Only create the tag if it hasn't already been created for this specific Proof class
if (SideloadedTag.get(tag.name) === undefined) {
computedTag = Pickles.sideLoaded.create(
tag.name,
Proof.maxProofsVerified,
Proof.publicInputType?.sizeInFields() ?? 0,
Proof.publicOutputType?.sizeInFields() ?? 0,
featureFlagsToMlOption(Proof.featureFlags, withRuntimeTables)
);
SideloadedTag.store(tag.name, computedTag);
} else {
computedTag = SideloadedTag.get(tag.name);
}
return { isSelf: false, tag: computedTag };
} else {
let compiledTag = CompiledTag.get(tag);
if (compiledTag === undefined) {
throw Error(
`${proofSystemTag.name}.compile() depends on ${tag.name}, but we cannot find compilation output for ${tag.name}.\n` +
`Try to run ${tag.name}.compile() first.`
);
}
return { isSelf: false, tag: compiledTag };
}
});
let featureFlags = featureFlagsToMlOption(featureFlagsFromGates(gates, withRuntimeTables));
return {
identifier: methodName,
main,
featureFlags,
proofsToVerify: MlArray.to(proofsToVerify),
};
}
function computeMaxProofsVerified(proofs: number[]) {
return proofs.reduce((acc: number, n) => {
assert(n <= 2, 'Too many proofs');
return Math.max(acc, n);
}, 0) as 0 | 1 | 2;
}
function fromFieldVars<T>(type: Provable<T>, fields: MlFieldArray, auxData: any[] = []) {
return type.fromFields(MlFieldArray.from(fields), auxData);
}
function fromFieldConsts<T>(type: Provable<T>, fields: MlFieldConstArray, aux: any[] = []) {
return type.fromFields(MlFieldConstArray.from(fields), aux);
}
function toFieldConsts<T>(type: Provable<T>, value: T) {
return MlFieldConstArray.to(type.toFields(value));
}
function toFieldAndAuxConsts<T>(type: Provable<T>, value: T) {
return {
publicInputFields: MlFieldConstArray.to(type.toFields(value)),
publicInputAux: type.toAuxiliary(value),
};
}
ZkProgram.Proof = function <
PublicInputType extends FlexibleProvable<any>,
PublicOutputType extends FlexibleProvable<any>,
>(program: {
name: string;
publicInputType: PublicInputType;
publicOutputType: PublicOutputType;
}): typeof Proof<InferProvable<PublicInputType>, InferProvable<PublicOutputType>> & {
provable: Provable<
Proof<InferProvable<PublicInputType>, InferProvable<PublicOutputType>>,
ProofValue<InferValue<PublicInputType>, InferValue<PublicOutputType>>
>;
} {
return class ZkProgramProof extends Proof<any, any> {
static publicInputType = program.publicInputType;
static publicOutputType = program.publicOutputType;
static tag = () => program;
};
};
let dummyProofCache: string | undefined;
async function dummyBase64Proof() {
if (dummyProofCache) return dummyProofCache;
let proof = await dummyProof(2, 15);
let base64Proof = Pickles.proofToBase64([2, proof]);
dummyProofCache = base64Proof;
return base64Proof;
}
// helpers for circuit context
function Prover<ProverData>() {
return {
async run<Result>(
witnesses: unknown[],
proverData: ProverData,
callback: () => Promise<Result>
) {
let id = snarkContext.enter({ witnesses, proverData, inProver: true });
try {
return await callback();
} finally {
snarkContext.leave(id);
}
},
getData(): ProverData {
return snarkContext.get().proverData;
},
};
}
// helper types
type Infer<T> =
T extends Subclass<typeof ProofBase>
? InstanceType<T>
: T extends ProvableType
? InferProvableType<T>
: never;
type TupleToInstances<T> = {
[I in keyof T]: Infer<T[I]>;
};
type TupleFrom<T> = {
[I in keyof T]: From<T[I]>;
};
type PrivateInput = ProvableType | Subclass<typeof ProofBase>;
type MethodReturnType<PublicOutput, AuxiliaryOutput> = PublicOutput extends void
? AuxiliaryOutput extends undefined
? void
: {
auxiliaryOutput: AuxiliaryOutput;
}
: AuxiliaryOutput extends undefined
? {
publicOutput: PublicOutput;
}
: {
publicOutput: PublicOutput;
auxiliaryOutput: AuxiliaryOutput;
};
type Method<
PublicInput,
PublicOutput,
MethodSignature extends {
privateInputs: Tuple<PrivateInput>;
auxiliaryOutput?: ProvableType;
},
> = PublicInput extends undefined
? {
method(
...args: TupleToInstances<MethodSignature['privateInputs']>
): Promise<
MethodReturnType<
PublicOutput,
InferProvableOrUndefined<Get<MethodSignature, 'auxiliaryOutput'>>
>
>;
}
: {
method(
publicInput: PublicInput,
...args: TupleToInstances<MethodSignature['privateInputs']>
): Promise<
MethodReturnType<
PublicOutput,
InferProvableOrUndefined<Get<MethodSignature, 'auxiliaryOutput'>>
>
>;
};
type RegularProver<
PublicInput,
PublicInputType,
PublicOutput,
Args extends Tuple<PrivateInput>,
AuxiliaryOutput,
> = (
publicInput: From<PublicInputType>,
...args: TupleFrom<Args>
) => Promise<{
proof: Proof<PublicInput, PublicOutput>;
auxiliaryOutput: AuxiliaryOutput;
}>;
type Prover<
PublicInput,
PublicInputType,
PublicOutput,
Args extends Tuple<PrivateInput>,
AuxiliaryOutput,
> = PublicInput extends undefined
? (...args: TupleFrom<Args>) => Promise<{
proof: Proof<PublicInput, PublicOutput>;
auxiliaryOutput: AuxiliaryOutput;
}>
: (
publicInput: From<PublicInputType>,
...args: TupleFrom<Args>
) => Promise<{
proof: Proof<PublicInput, PublicOutput>;
auxiliaryOutput: AuxiliaryOutput;
}>;
type ProvableOrUndefined<A> = A extends undefined ? typeof Undefined : ToProvable<A>;
type ProvableOrVoid<A> = A extends undefined ? typeof Void : ToProvable<A>;
type InferProvableOrUndefined<A> = A extends undefined
? undefined
: A extends ProvableType
? InferProvable<A>
: InferProvable<A> | undefined;
type InferProvableOrVoid<A> = A extends undefined ? void : InferProvable<A>;
type UnwrapPromise<P> = P extends Promise<infer T> ? T : never;