o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
602 lines (566 loc) • 18.6 kB
text/typescript
/**
* {@link Provable} is
* - a namespace with tools for writing provable code
* - the main interface for types that can be used in provable code
*/
import { Bool } from './bool.js';
import { Field } from './field.js';
import type { Provable as Provable_ } from './types/provable-intf.js';
import type { FlexibleProvable, ProvableExtended } from './types/struct.js';
import { Context } from '../util/global-context.js';
import {
HashInput,
InferJson,
InferProvable,
InferredProvable,
} from './types/provable-derivers.js';
import {
inCheckedComputation,
inProver,
asProver,
constraintSystem,
generateWitness,
} from './core/provable-context.js';
import { witness, witnessAsync, witnessFields } from './types/witness.js';
import { InferValue } from '../../bindings/lib/provable-generic.js';
// external API
export { Provable };
// internal API
export {
memoizationContext,
MemoizationContext,
memoizeWitness,
getBlindingValue,
};
/**
* `Provable<T>` is the general interface for provable types in o1js.
*
* `Provable<T>` describes how a type `T` is made up of {@link Field} elements and "auxiliary" (non-provable) data.
*
* `Provable<T>` is the required input type in several methods in o1js.
* One convenient way to create a `Provable<T>` is using `Struct`.
*
* All built-in provable types in o1js ({@link Field}, {@link Bool}, etc.) are instances of `Provable<T>` as well.
*
* Note: These methods are meant to be used by the library internally and are not directly when writing provable code.
*/
type Provable<T, TValue = any> = Provable_<T, TValue>;
const Provable = {
/**
* Create a new witness. A witness, or variable, is a value that is provided as input
* by the prover. This provides a flexible way to introduce values from outside into the circuit.
* However, note that nothing about how the value was created is part of the proof - `Provable.witness`
* behaves exactly like user input. So, make sure that after receiving the witness you make any assertions
* that you want to associate with it.
* @example
* Example for re-implementing `Field.inv` with the help of `witness`:
* ```ts
* let invX = Provable.witness(Field, () => {
* // compute the inverse of `x` outside the circuit, however you like!
* return Field.inv(x);
* }
* // prove that `invX` is really the inverse of `x`:
* invX.mul(x).assertEquals(1);
* ```
*/
witness,
/**
* Witness a tuple of field elements. This works just like {@link Provable.witness},
* but optimized for witnessing plain field elements, which is especially common
* in low-level provable code.
*/
witnessFields,
/**
* Create a new witness from an async callback.
*
* See {@link Provable.witness} for more information.
*/
witnessAsync,
/**
* Proof-compatible if-statement.
* This behaves like a ternary conditional statement in JS.
*
* **Warning**: Since `Provable.if()` is a normal JS function call, both the if and the else branch
* are evaluated before calling it. Therefore, you can't use this function
* to guard against execution of one of the branches. It only allows you to pick one of two values.
*
* @example
* ```ts
* const condition = Bool(true);
* const result = Provable.if(condition, Field(1), Field(2)); // returns Field(1)
* ```
*/
if: if_,
/**
* Generalization of {@link Provable.if} for choosing between more than two different cases.
* It takes a "mask", which is an array of `Bool`s that contains only one `true` element, a type/constructor, and an array of values of that type.
* The result is that value which corresponds to the true element of the mask.
* @example
* ```ts
* let x = Provable.switch([Bool(false), Bool(true)], Field, [Field(1), Field(2)]);
* x.assertEquals(2);
* ```
*/
switch: switch_,
/**
* Asserts that two values are equal.
* @example
* ```ts
* class MyStruct extends Struct({ a: Field, b: Bool }) {};
* const a: MyStruct = { a: Field(0), b: Bool(false) };
* const b: MyStruct = { a: Field(1), b: Bool(true) };
* Provable.assertEqual(MyStruct, a, b);
* ```
*/
assertEqual,
/**
* Asserts that two values are equal, if an enabling condition is true.
*
* If the condition is false, the assertion is skipped.
*/
assertEqualIf,
/**
* Checks if two elements are equal.
* @example
* ```ts
* class MyStruct extends Struct({ a: Field, b: Bool }) {};
* const a: MyStruct = { a: Field(0), b: Bool(false) };
* const b: MyStruct = { a: Field(1), b: Bool(true) };
* const isEqual = Provable.equal(MyStruct, a, b);
* ```
*/
equal,
/**
* Creates a {@link Provable} for a generic array.
* @example
* ```ts
* const ProvableArray = Provable.Array(Field, 5);
* ```
*/
Array: provableArray,
/**
* Check whether a value is constant.
* See {@link FieldVar} for more information about constants and variables.
*
* @example
* ```ts
* let x = Field(42);
* Provable.isConstant(Field, x); // true
* ```
*/
isConstant,
/**
* Interface to log elements within a circuit. Similar to `console.log()`.
* @example
* ```ts
* const element = Field(42);
* Provable.log(element);
* ```
*/
log,
/**
* Runs code as a prover.
* @example
* ```ts
* Provable.asProver(() => {
* // Your prover code here
* });
* ```
*/
asProver,
/**
* Runs provable code quickly, without creating a proof, but still checking whether constraints are satisfied.
* @example
* ```ts
* await Provable.runAndCheck(() => {
* // Your code to check here
* });
* ```
*/
async runAndCheck(f: (() => Promise<void>) | (() => void)) {
await generateWitness(f, { checkConstraints: true });
},
/**
* Runs provable code quickly, without creating a proof, and not checking whether constraints are satisfied.
* @example
* ```ts
* await Provable.runUnchecked(() => {
* // Your code to run here
* });
* ```
*/
async runUnchecked(f: (() => Promise<void>) | (() => void)) {
await generateWitness(f, { checkConstraints: false });
},
/**
* Returns information about the constraints created by the callback function.
* @example
* ```ts
* const result = await Provable.constraintSystem(circuit);
* console.log(result);
* ```
*/
constraintSystem,
/**
* Checks if the code is run in prover mode.
* @example
* ```ts
* if (Provable.inProver()) {
* // Prover-specific code
* }
* ```
*/
inProver,
/**
* Checks if the code is run in checked computation mode.
* @example
* ```ts
* if (Provable.inCheckedComputation()) {
* // Checked computation-specific code
* }
* ```
*/
inCheckedComputation,
/**
* Returns a constant version of a provable type.
*/
toConstant<T>(type: Provable<T>, value: T) {
return type.fromFields(
type.toFields(value).map((x) => x.toConstant()),
type.toAuxiliary(value)
);
},
};
type ToFieldable = { toFields(): Field[] };
// general provable methods
function assertEqual<T>(type: FlexibleProvable<T>, x: T, y: T): void;
function assertEqual<T extends ToFieldable>(x: T, y: T): void;
function assertEqual(typeOrX: any, xOrY: any, yOrUndefined?: any) {
if (yOrUndefined === undefined) {
return assertEqualImplicit(typeOrX, xOrY);
} else {
return assertEqualExplicit(typeOrX, xOrY, yOrUndefined);
}
}
function assertEqualImplicit<T extends ToFieldable>(x: T, y: T) {
let xs = x.toFields();
let ys = y.toFields();
let n = checkLength('Provable.assertEqual', xs, ys);
for (let i = 0; i < n; i++) {
xs[i].assertEquals(ys[i]);
}
}
function assertEqualExplicit<T>(type: Provable<T>, x: T, y: T) {
let xs = type.toFields(x);
let ys = type.toFields(y);
for (let i = 0; i < xs.length; i++) {
xs[i].assertEquals(ys[i]);
}
}
function equal<T>(type: FlexibleProvable<T>, x: T, y: T): Bool;
function equal<T extends ToFieldable>(x: T, y: T): Bool;
function equal(typeOrX: any, xOrY: any, yOrUndefined?: any) {
if (yOrUndefined === undefined) {
return equalImplicit(typeOrX, xOrY);
} else {
return equalExplicit(typeOrX, xOrY, yOrUndefined);
}
}
// TODO: constraints can be reduced by up to 2x for large structures by using a variant
// of the `equals` argument where we return 1 - z(x0 - y0)(x1 - y1)...(xn - yn)
// current version will do (1 - z0(x0 - y0))(1 - z1(x1 - y1))... + constrain each factor
function equalImplicit<T extends ToFieldable>(x: T, y: T) {
let xs = x.toFields();
let ys = y.toFields();
checkLength('Provable.equal', xs, ys);
return xs.map((x, i) => x.equals(ys[i])).reduce(Bool.and);
}
function equalExplicit<T>(type: Provable<T>, x: T, y: T) {
let xs = type.toFields(x);
let ys = type.toFields(y);
return xs.map((x, i) => x.equals(ys[i])).reduce(Bool.and);
}
function if_<T>(condition: Bool, type: FlexibleProvable<T>, x: T, y: T): T;
function if_<T extends ToFieldable>(condition: Bool, x: T, y: T): T;
function if_(condition: Bool, typeOrX: any, xOrY: any, yOrUndefined?: any) {
if (yOrUndefined === undefined) {
return ifImplicit(condition, typeOrX, xOrY);
} else {
return ifExplicit(condition, typeOrX, xOrY, yOrUndefined);
}
}
function ifField(b: Field, x: Field, y: Field) {
// TODO: this is suboptimal if one of x, y is constant
// it uses 2-3 generic gates in that case, where 1 would be enough
// b*(x - y) + y
// NOTE: the R1CS constraint used by Field.if_ in snarky-ml
// leads to a different but equivalent layout (same # constraints)
// https://github.com/o1-labs/snarky/blob/14f8e2ff981a9c9ea48c94b2cc1d8c161301537b/src/base/utils.ml#L171
// in the case x, y are constant, the layout is the same
return b.mul(x.sub(y)).add(y).seal();
}
function ifExplicit<T>(condition: Bool, type: Provable<T>, x: T, y: T): T {
let xs = type.toFields(x);
let ys = type.toFields(y);
let b = condition.toField();
// simple case: b is constant - it's like a normal if statement
if (b.isConstant()) {
return clone(type, condition.toBoolean() ? x : y);
}
// if b is variable, we compute if as follows:
// if(b, x, y)[i] = b*(x[i] - y[i]) + y[i]
let fields = xs.map((xi, i) => ifField(b, xi, ys[i]));
let aux = auxiliary(type, () => (condition.toBoolean() ? x : y));
return type.fromFields(fields, aux);
}
function ifImplicit<T extends ToFieldable>(condition: Bool, x: T, y: T): T {
let type = x.constructor;
if (type === undefined)
throw Error(
`You called Provable.if(bool, x, y) with an argument x that has no constructor, which is not supported.\n` +
`If x, y are Structs or other custom types, you can use the following:\n` +
`Provable.if(bool, MyType, x, y)`
);
if (type !== y.constructor) {
throw Error(
'Provable.if: Mismatched argument types. Try using an explicit type argument:\n' +
`Provable.if(bool, MyType, x, y)`
);
}
if (!('fromFields' in type && 'toFields' in type)) {
throw Error(
'Provable.if: Invalid argument type. Try using an explicit type argument:\n' +
`Provable.if(bool, MyType, x, y)`
);
}
return ifExplicit(condition, type as any as Provable<T>, x, y);
}
function switch_<T, A extends FlexibleProvable<T>>(
mask: Bool[],
type: A,
values: T[],
{ allowNonExclusive = false } = {}
): T {
// picks the value at the index where mask is true
let nValues = values.length;
if (mask.length !== nValues)
throw Error(
`Provable.switch: \`values\` and \`mask\` have different lengths (${values.length} vs. ${mask.length}), which is not allowed.`
);
let checkMask = () => {
if (allowNonExclusive) return;
let nTrue = mask.filter((b) => b.toBoolean()).length;
if (nTrue > 1) {
throw Error(
`Provable.switch: \`mask\` must have 0 or 1 true element, found ${nTrue}.`
);
}
};
if (mask.every((b) => b.toField().isConstant())) checkMask();
else Provable.asProver(checkMask);
let size = type.sizeInFields();
let fields = Array(size).fill(new Field(0));
for (let i = 0; i < nValues; i++) {
let valueFields = type.toFields(values[i]);
let maskField = mask[i].toField();
for (let j = 0; j < size; j++) {
let maybeField = valueFields[j].mul(maskField);
fields[j] = fields[j].add(maybeField);
}
}
let aux = auxiliary(type as Provable<T>, () => {
let i = mask.findIndex((b) => b.toBoolean());
if (i === -1) return undefined;
return values[i];
});
return (type as Provable<T>).fromFields(fields, aux);
}
function assertEqualIf<
A extends Provable<any>,
T extends InferProvable<A> = InferProvable<A>
>(enabled: Bool, type: A, x: T, y: T) {
// if the condition is disabled, we check the trivial identity x === x instead
let xOrY = ifExplicit<T>(enabled, type, y, x);
assertEqual(type, x, xOrY);
}
function isConstant<T>(type: Provable<T>, x: T): boolean {
return type.toFields(x).every((x) => x.isConstant());
}
// logging in provable code
function log(...args: any) {
asProver(() => {
let prettyArgs = [];
for (let arg of args) {
if (arg?.toPretty !== undefined) prettyArgs.push(arg.toPretty());
else {
try {
prettyArgs.push(JSON.parse(JSON.stringify(arg)));
} catch {
prettyArgs.push(arg);
}
}
}
console.log(...prettyArgs);
});
}
// helpers
function checkLength(name: string, xs: Field[], ys: Field[]) {
let n = xs.length;
let m = ys.length;
if (n !== m) {
throw Error(
`${name}: inputs must contain the same number of field elements, got ${n} !== ${m}`
);
}
return n;
}
function clone<T, S extends FlexibleProvable<T>>(type: S, value: T): T {
let fields = type.toFields(value);
let aux = type.toAuxiliary?.(value) ?? [];
return (type as Provable<T>).fromFields(fields, aux);
}
function auxiliary<T>(type: Provable<T>, compute: () => T | undefined) {
let aux;
// TODO: this accepts types without .toAuxiliary(), should be changed when all snarky types are moved to TS
Provable.asProver(() => {
let value = compute();
if (value !== undefined) {
aux = type.toAuxiliary?.(value);
}
});
return aux ?? type.toAuxiliary?.() ?? [];
}
type MemoizationContext = {
memoized: { fields: Field[]; aux: any[] }[];
currentIndex: number;
blindingValue: Field;
};
let memoizationContext = Context.create<MemoizationContext>();
/**
* Like Provable.witness, but memoizes the witness during transaction construction
* for reuse by the prover. This is needed to witness non-deterministic values.
*/
function memoizeWitness<T>(type: FlexibleProvable<T>, compute: () => T) {
return Provable.witness(type as Provable<T>, () => {
if (!memoizationContext.has()) return compute();
let context = memoizationContext.get();
let { memoized, currentIndex } = context;
let currentValue = memoized[currentIndex];
if (currentValue === undefined) {
let value = compute();
let fields = type.toFields(value).map((x) => x.toConstant());
let aux = type.toAuxiliary(value);
currentValue = { fields, aux };
memoized[currentIndex] = currentValue;
}
context.currentIndex += 1;
return (type as Provable<T>).fromFields(
currentValue.fields,
currentValue.aux
);
});
}
function getBlindingValue() {
if (!memoizationContext.has()) return Field.random();
let context = memoizationContext.get();
if (context.blindingValue === undefined) {
context.blindingValue = Field.random();
}
return context.blindingValue;
}
// TODO this should return a class, like Struct, so you can just use `class Array3 extends Provable.Array(Field, 3) {}`
function provableArray<A extends FlexibleProvable<any>>(
elementType: A,
length: number
): InferredProvable<A[]> {
type T = InferProvable<A>;
type TValue = InferValue<A>;
type TJson = InferJson<A>;
let type = elementType as ProvableExtended<T, TValue, TJson>;
return {
/**
* Returns the size of this structure in {@link Field} elements.
* @returns size of this structure
*/
sizeInFields() {
let elementLength = type.sizeInFields();
return elementLength * length;
},
/**
* Serializes this structure into {@link Field} elements.
* @returns an array of {@link Field} elements
*/
toFields(array: T[]) {
return array.map((e) => type.toFields(e)).flat();
},
/**
* Serializes this structure's auxiliary data.
* @returns auxiliary data
*/
toAuxiliary(array?) {
let array_ = array ?? Array<undefined>(length).fill(undefined);
return array_?.map((e) => type.toAuxiliary(e));
},
/**
* Deserializes an array of {@link Field} elements into this structure.
*/
fromFields(fields: Field[], aux?: any[]) {
let array = [];
let size = type.sizeInFields();
let n = length;
for (let i = 0, offset = 0; i < n; i++, offset += size) {
array[i] = type.fromFields(
fields.slice(offset, offset + size),
aux?.[i]
);
}
return array;
},
check(array: T[]) {
for (let i = 0; i < length; i++) {
(type as any).check(array[i]);
}
},
toValue(x) {
return x.map((v) => type.toValue(v));
},
fromValue(x) {
return x.map((v) => type.fromValue(v));
},
/**
* Encodes this structure into a JSON-like object.
*/
toJSON(array) {
if (!('toJSON' in type)) {
throw Error('circuitArray.toJSON: element type has no toJSON method');
}
return array.map((v) => type.toJSON(v));
},
/**
* Decodes a JSON-like object into this structure.
*/
fromJSON(json) {
if (!('fromJSON' in type)) {
throw Error(
'circuitArray.fromJSON: element type has no fromJSON method'
);
}
return json.map((a) => type.fromJSON(a));
},
toInput(array) {
if (!('toInput' in type)) {
throw Error('circuitArray.toInput: element type has no toInput method');
}
return array.reduce(
(curr, value) => HashInput.append(curr, type.toInput(value)),
HashInput.empty
);
},
empty() {
if (!('empty' in type)) {
throw Error('circuitArray.empty: element type has no empty() method');
}
return Array.from({ length }, () => type.empty());
},
} satisfies ProvableExtended<T[], TValue[], TJson[]> as any;
}