o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
463 lines (420 loc) • 13.1 kB
text/typescript
/**
* DSL for testing that a gadget generates the expected constraint system.
*
* An essential feature is that `constraintSystem()` automatically generates a
* variety of fieldvar types for the inputs: constants, variables, and combinators.
*/
import { Gate, GateType } from '../../snarky.js';
import { randomBytes } from '../../bindings/crypto/random.js';
import { Field } from '../provable/field.js';
import { FieldType, FieldVar } from '../provable/core/fieldvar.js';
import { Provable } from '../provable/provable.js';
import { Tuple } from '../util/types.js';
import { Random } from './random.js';
import { test } from './property.js';
import { Undefined, ZkProgram } from '../proof-system/zkprogram.js';
import {
printGates,
summarizeGates,
synchronousRunners,
} from '../provable/core/provable-context.js';
export {
constraintSystem,
not,
and,
or,
fulfills,
equals,
contains,
allConstant,
ifNotAllConstant,
isEmpty,
withoutGenerics,
print,
repeat,
ConstraintSystemTest,
};
// TODO get rid of this top-level await by making `test` support async functions
let { constraintSystemSync } = await synchronousRunners();
/**
* `constraintSystem()` is a test runner to check properties of constraint systems.
* You give it a description of inputs and a circuit, as well as a `ConstraintSystemTest` to assert
* properties on the generated constraint system.
*
* As input variables, we generate random combinations of constants, variables, add & scale combinators,
* to poke for the common problem of gate chains broken by unexpected Generic gates.
*
* The `constraintSystemTest` is written using a DSL of property assertions, such as {@link equals} and {@link contains}.
* To run multiple assertions, use the {@link and} / {@link or} combinators.
* To debug the constraint system, use the {@link print} test or `and(print, ...otherTests)`.
*
* @param label description of the constraint system
* @param inputs input spec in form `{ from: [...provables] }`
* @param main circuit to test
* @param constraintSystemTest property test to run on the constraint system
*/
function constraintSystem<Input extends Tuple<CsVarSpec<any>>>(
label: string,
inputs: { from: Input },
main: (...args: CsParams<Input>) => void,
constraintSystemTest: ConstraintSystemTest
) {
// create random generators
let types = inputs.from.map(provable);
let rngs = types.map(layout);
test(...rngs, (...args) => {
let layouts = args.slice(0, -1);
// compute the constraint system
let { gates } = constraintSystemSync(() => {
// each random input "layout" has to be instantiated into vars in this circuit
let values = types.map((type, i) =>
instantiate(type, layouts[i])
) as CsParams<Input>;
main(...values);
});
// run tests
let typesAndValues = types.map((type, i) => ({ type, value: layouts[i] }));
let { ok, failures } = run(constraintSystemTest, gates, typesAndValues);
if (!ok) {
console.log('Constraint system:');
printGates(gates);
throw Error(
`Constraint system test: ${label}\n\n${failures
.map((f) => `FAIL: ${f}`)
.join('\n')}\n`
);
}
});
}
/**
* Convenience function to run {@link constraintSystem} on the method of a {@link ZkProgram}.
*
* @example
* ```ts
* const program = ZkProgram({ methods: { myMethod: ... }, ... });
*
* constraintSystem.fromZkProgram(program, 'myMethod', contains('Rot64'));
* ```
*/
constraintSystem.fromZkProgram = function fromZkProgram<
T,
K extends keyof T & string
>(
program: { privateInputTypes: T },
methodName: K,
test: ConstraintSystemTest
) {
let program_: ZkProgram<any, any> = program as any;
let from: any = [...program_.privateInputTypes[methodName]];
if (program_.publicInputType !== Undefined) {
from.unshift(program_.publicInputType);
}
return constraintSystem(
`${program_.name} / ${methodName}()`,
{ from },
program_.rawMethods[methodName],
test
);
};
// DSL for writing tests
type ConstraintSystemTestBase = {
run: (cs: Gate[], inputs: TypeAndValue<any>[]) => boolean;
label: string;
};
type Base = { kind?: undefined } & ConstraintSystemTestBase;
type Not = { kind: 'not' } & ConstraintSystemTestBase;
type And = { kind: 'and'; tests: ConstraintSystemTest[]; label: string };
type Or = { kind: 'or'; tests: ConstraintSystemTest[]; label: string };
type ConstraintSystemTest = Base | Not | And | Or;
type Result = { ok: boolean; failures: string[] };
function run(
test: ConstraintSystemTest,
cs: Gate[],
inputs: TypeAndValue<any>[]
): Result {
switch (test.kind) {
case undefined: {
let ok = test.run(cs, inputs);
let failures = ok ? [] : [test.label];
return { ok, failures };
}
case 'not': {
let ok = test.run(cs, inputs);
let failures = ok ? [`not(${test.label})`] : [];
return { ok: !ok, failures };
}
case 'and': {
let results = test.tests.map((t) => run(t, cs, inputs));
let ok = results.every((r) => r.ok);
let failures = ok ? [] : results.flatMap((r) => r.failures);
return { ok, failures };
}
case 'or': {
let results = test.tests.map((t) => run(t, cs, inputs));
let ok = results.some((r) => r.ok);
let failures = ok ? [] : results.flatMap((r) => r.failures);
return { ok, failures };
}
}
}
/**
* Negate a test.
*/
function not(test: ConstraintSystemTest): ConstraintSystemTest {
return { kind: 'not', ...test };
}
/**
* Check that all input tests pass.
*/
function and(...tests: ConstraintSystemTest[]): ConstraintSystemTest {
return { kind: 'and', tests, label: `and(${tests.map((t) => t.label)})` };
}
/**
* Check that at least one input test passes.
*/
function or(...tests: ConstraintSystemTest[]): ConstraintSystemTest {
return { kind: 'or', tests, label: `or(${tests.map((t) => t.label)})` };
}
/**
* General test
*/
function fulfills(
label: string,
run: (cs: Gate[], inputs: TypeAndValue<any>[]) => boolean
): ConstraintSystemTest {
return { run, label };
}
/**
* Test for precise equality of the constraint system with a given list of gates.
*/
function equals(gates: readonly GateType[]): ConstraintSystemTest {
return {
run(cs) {
if (cs.length !== gates.length) return false;
return cs.every((g, i) => g.type === gates[i]);
},
label: `equals ${JSON.stringify(gates)}`,
};
}
/**
* Test that constraint system contains each of a list of gates consecutively.
*
* You can also pass a list of lists. In that case, the constraint system has to contain
* each of the lists of gates in the given order, but not necessarily consecutively.
*
* @example
* ```ts
* // constraint system contains a Rot64 gate
* contains('Rot64')
*
* // constraint system contains a Rot64 gate, followed directly by a RangeCheck0 gate
* contains(['Rot64', 'RangeCheck0'])
*
* // constraint system contains two instances of the combination [Rot64, RangeCheck0]
* contains([['Rot64', 'RangeCheck0'], ['Rot64', 'RangeCheck0']]])
* ```
*/
function contains(
gates: GateType | readonly GateType[] | readonly GateType[][]
): ConstraintSystemTest {
let expectedGatess = toGatess(gates);
return {
run(cs) {
let gates = cs.map((g) => g.type);
let i = 0;
let j = 0;
for (let gate of gates) {
if (gate === expectedGatess[i][j]) {
j++;
if (j === expectedGatess[i].length) {
i++;
j = 0;
if (i === expectedGatess.length) return true;
}
} else if (gate === expectedGatess[i][0]) {
j = 1;
} else {
j = 0;
}
}
return false;
},
label: `contains ${JSON.stringify(expectedGatess)}`,
};
}
/**
* Test whether all inputs are constant.
*/
const allConstant: ConstraintSystemTest = {
run(cs, inputs) {
return inputs.every(({ type, value }) =>
type.toFields(value).every((x) => x.isConstant())
);
},
label: 'all inputs constant',
};
/**
* Modifies a test so that it doesn't fail if all inputs are constant, and instead
* checks that the constraint system is empty in that case.
*/
function ifNotAllConstant(test: ConstraintSystemTest): ConstraintSystemTest {
return or(test, and(allConstant, isEmpty));
}
/**
* Test whether constraint system is empty.
*/
const isEmpty = fulfills('constraint system is empty', (cs) => cs.length === 0);
/**
* Modifies a test so that it runs on the constraint system with generic gates filtered out.
*/
function withoutGenerics(test: ConstraintSystemTest): ConstraintSystemTest {
return {
run(cs, inputs) {
return run(
test,
cs.filter((g) => g.type !== 'Generic'),
inputs
).ok;
},
label: `withoutGenerics(${test.label})`,
};
}
/**
* "Test" that just pretty-prints the constraint system.
*/
const print: ConstraintSystemTest = {
run(cs) {
console.log('Constraint system:');
printGates(cs);
return true;
},
label: '',
};
// Do other useful things with constraint systems
/**
* Get constraint system as a list of gates.
*/
constraintSystem.gates = function gates<Input extends Tuple<CsVarSpec<any>>>(
inputs: { from: Input },
main: (...args: CsParams<Input>) => void
) {
let types = inputs.from.map(provable);
let { gates } = constraintSystemSync(() => {
let values = types.map((type) =>
Provable.witness(type, (): unknown => {
throw Error('not needed');
})
) as CsParams<Input>;
main(...values);
});
return gates;
};
function map<T>(transform: (gates: Gate[]) => T) {
return <Input extends Tuple<CsVarSpec<any>>>(
inputs: { from: Input },
main: (...args: CsParams<Input>) => void
) => transform(constraintSystem.gates(inputs, main));
}
/**
* Get size of constraint system.
*/
constraintSystem.size = map((gates) => gates.length);
/**
* Print constraint system.
*/
constraintSystem.print = map(printGates);
/**
* Get constraint system summary.
*/
constraintSystem.summary = map(summarizeGates);
function repeat(
n: number,
gates: GateType | readonly GateType[]
): readonly GateType[] {
gates = Array.isArray(gates) ? gates : [gates];
return Array<readonly GateType[]>(n).fill(gates).flat();
}
function toGatess(
gateTypes: GateType | readonly GateType[] | readonly GateType[][]
): GateType[][] {
if (typeof gateTypes === 'string') return [[gateTypes]];
if (Array.isArray(gateTypes[0])) return gateTypes as GateType[][];
return [gateTypes as GateType[]];
}
// Random generator for arbitrary provable types
function provable<T>(spec: CsVarSpec<T>): Provable<T, any> {
return 'provable' in spec ? spec.provable : spec;
}
function layout<T>(type: Provable<T, any>): Random<T> {
let length = type.sizeInFields();
return Random(() => {
let fields = Array.from({ length }, () => new Field(drawFieldVar()));
return type.fromFields(fields, type.toAuxiliary());
});
}
function instantiate<T>(type: Provable<T, any>, value: T) {
let fields = type.toFields(value).map((x) => instantiateFieldVar(x.value));
return type.fromFields(fields, type.toAuxiliary());
}
// Random generator for fieldvars that exercises constants, variables and combinators
function drawFieldVar(): FieldVar {
let fieldType = drawFieldType();
switch (fieldType) {
case FieldType.Constant: {
return FieldVar.constant(1n);
}
case FieldType.Var: {
return [FieldType.Var, 0];
}
case FieldType.Add: {
let x = drawFieldVar();
let y = drawFieldVar();
// prevent blow-up of constant size
if (x[0] === FieldType.Constant && y[0] === FieldType.Constant) return x;
return FieldVar.add(x, y);
}
case FieldType.Scale: {
let x = drawFieldVar();
// prevent blow-up of constant size
if (x[0] === FieldType.Constant) return x;
return FieldVar.scale(3n, x);
}
}
}
function instantiateFieldVar(x: FieldVar): Field {
switch (x[0]) {
case FieldType.Constant: {
return new Field(x);
}
case FieldType.Var: {
return Provable.witness(Field, () => Field.from(0n));
}
case FieldType.Add: {
let a = instantiateFieldVar(x[1]);
let b = instantiateFieldVar(x[2]);
return a.add(b);
}
case FieldType.Scale: {
let a = instantiateFieldVar(x[2]);
return a.mul(x[1][1]);
}
}
}
function drawFieldType(): FieldType {
let oneOf8 = randomBytes(1)[0] & 0b111;
if (oneOf8 < 4) return FieldType.Var;
if (oneOf8 < 6) return FieldType.Constant;
if (oneOf8 === 6) return FieldType.Scale;
return FieldType.Add;
}
// types
type CsVarSpec<T> = Provable<T, any> | { provable: Provable<T, any> };
type InferCsVar<T> = T extends { provable: Provable<infer U, any> }
? U
: T extends Provable<infer U, any>
? U
: never;
type CsParams<In extends Tuple<CsVarSpec<any>>> = {
[k in keyof In]: InferCsVar<In[k]>;
};
type TypeAndValue<T> = { type: Provable<T, any>; value: T };