o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
218 lines (182 loc) • 6.42 kB
text/typescript
import { Field, Bool } from '../provable/wrapped.js';
import { Struct } from '../provable/types/struct.js';
import { UInt64 } from '../provable/int.js';
import {
CompiledTag,
Empty,
Void,
ZkProgram,
picklesRuleFromFunction,
sortMethodArguments,
} from './zkprogram.js';
import { Proof } from './proof.js';
import { expect } from 'expect';
import { Pickles, Snarky } from '../../bindings.js';
import { AnyFunction } from '../util/types.js';
import { snarkContext } from '../provable/core/provable-context.js';
import { it } from 'node:test';
import { Provable } from '../provable/provable.js';
import { bool, equivalentAsync, field, record } from '../testing/equivalent.js';
import { FieldVar, FieldConst } from '../provable/core/fieldvar.js';
import { ProvablePure } from '../provable/types/provable-intf.js';
const EmptyProgram = ZkProgram({
name: 'empty',
publicInput: Field,
methods: { run: { privateInputs: [], async method(_) {} } },
});
class EmptyProof extends EmptyProgram.Proof {}
// unit-test zkprogram creation helpers:
// -) sortMethodArguments
// -) picklesRuleFromFunction
it('pickles rule creation', async () => {
// a rule that verifies a proof conditionally, and returns the proof's input as output
function main(proof: EmptyProof, shouldVerify: Bool) {
proof.verifyIf(shouldVerify);
return {
publicOutput: proof.publicInput,
};
}
let privateInputs = [EmptyProof, Bool];
// collect method interface
let methodIntf = sortMethodArguments('mock', 'main', privateInputs, undefined, Proof);
expect(methodIntf).toEqual({
methodName: 'main',
args: [EmptyProof, Bool],
});
// store compiled tag
CompiledTag.store(EmptyProgram, 'mock tag');
// create pickles rule
let rule: Pickles.Rule = picklesRuleFromFunction(
Empty as ProvablePure<any>,
Field as ProvablePure<any>,
main as AnyFunction,
{ name: 'mock' },
methodIntf,
[],
[EmptyProof]
);
await equivalentAsync({ from: [field, bool], to: record({ field, bool }) }, { runs: 5 })(
(field, bool) => ({ field, bool }),
async (field, bool) => {
let dummy = await EmptyProof.dummy(field, undefined, 0);
let field_: FieldConst = [0, 0n];
let bool_: FieldConst = [0, 0n];
await Provable.runAndCheck(async () => {
// put witnesses in snark context
snarkContext.get().witnesses = [dummy, bool];
// call pickles rule
let {
publicOutput: [, publicOutput],
shouldVerify: [, shouldVerify],
} = await rule.main([0]);
// `publicOutput` and `shouldVerify` are as expected
Snarky.field.assertEqual(publicOutput, dummy.publicInput.value);
Snarky.field.assertEqual(shouldVerify, bool.value);
Provable.asProver(() => {
field_ = Snarky.field.readVar(publicOutput);
bool_ = Snarky.field.readVar(shouldVerify);
});
});
return { field: Field(field_), bool: Bool(FieldVar.constant(bool_)) };
}
);
});
class NestedProof extends Struct({ proof: EmptyProof, field: Field }) {}
const NestedProof2 = Provable.Array(NestedProof, 2);
// type inference
NestedProof satisfies Provable<{ proof: Proof<Field, void>; field: Field }>;
it('pickles rule creation: nested proof', async () => {
function main([first, _second]: [NestedProof, NestedProof]) {
// first proof should verify, second should not
first.proof.verify();
// deep type inference
first.proof.publicInput satisfies Field;
first.proof.publicOutput satisfies void;
}
// collect method interface
let methodIntf = sortMethodArguments('mock', 'main', [NestedProof2], undefined, Proof);
expect(methodIntf).toEqual({
methodName: 'main',
args: [NestedProof2],
});
// store compiled tag
CompiledTag.store(EmptyProgram, 'mock tag');
// create pickles rule
let rule: Pickles.Rule = picklesRuleFromFunction(
Empty as ProvablePure<any>,
Void as ProvablePure<any>,
main as AnyFunction,
{ name: 'mock' },
methodIntf,
[],
[EmptyProof, EmptyProof]
);
let dummy = await EmptyProof.dummy(Field(0), undefined, 0);
let nested1 = new NestedProof({ proof: dummy, field: Field(0) });
let nested2 = new NestedProof({ proof: dummy, field: Field(0) });
let nested = [nested1, nested2];
await Provable.runAndCheck(async () => {
// put witnesses in snark context
snarkContext.get().witnesses = [nested];
// call pickles rule
let {
shouldVerify: [, shouldVerify1, shouldVerify2],
previousStatements: [, ...previousStatements],
} = await rule.main([0]);
expect(previousStatements.length).toBe(2);
// `shouldVerify` are as expected
expect(Bool(shouldVerify1).isConstant()).toBe(true);
expect(Bool(shouldVerify2).isConstant()).toBe(true);
// first proof should verify, second should not
Bool(shouldVerify1).assertTrue();
Bool(shouldVerify2).assertFalse();
});
});
it('fails with more than two (nested) proofs', async () => {
expect(() => {
sortMethodArguments('mock', 'main', [NestedProof2, NestedProof], undefined, Proof);
}).toThrowError('mock.main() has more than two proof arguments');
});
// compile works with large inputs
const N = 100_000;
const program = ZkProgram({
name: 'large-array-program',
methods: {
baseCase: {
privateInputs: [Provable.Array(Field, N)],
async method(_: Field[]) {},
},
},
});
it('can compile program with large input', async () => {
await program.compile();
});
// regression tests for some zkprograms
const emptyMethodsMetadata = await EmptyProgram.analyzeMethods();
expect(emptyMethodsMetadata.run).toEqual(
expect.objectContaining({
rows: 0,
digest: '4f5ddea76d29cfcfd8c595f14e31f21b',
gates: [],
publicInputSize: 0,
})
);
class CounterPublicInput extends Struct({
current: UInt64,
updated: UInt64,
}) {}
const CounterProgram = ZkProgram({
name: 'counter',
publicInput: CounterPublicInput,
methods: {
increment: {
privateInputs: [UInt64],
async method({ current, updated }: CounterPublicInput, incrementBy: UInt64) {
const newCount = current.add(incrementBy);
newCount.assertEquals(updated);
},
},
},
});
const incrementMethodMetadata = (await CounterProgram.analyzeMethods()).increment;
expect(incrementMethodMetadata).toEqual(expect.objectContaining({ rows: 18 }));