diff --git a/CHANGELOG.md b/CHANGELOG.md index 65943f2a2b..2eb508bf0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `Lightnet` namespace to interact with the account manager provided by the [lightnet Mina network](https://hub.docker.com/r/o1labs/mina-local-network). https://github.com/o1-labs/o1js/pull/1167 +- Internal support for several custom gates (range check, bitwise operations, foreign field operations) and lookup tables https://github.com/o1-labs/o1js/pull/1176 + ## [0.13.1](https://github.com/o1-labs/o1js/compare/c2f392fe5...045faa7) ### Breaking changes diff --git a/src/bindings b/src/bindings index 046712cc50..851d3df238 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 046712cc50e59a6c31e5f63952fee132baa145b0 +Subproject commit 851d3df2385c693bd4f652ad7a0a1af98491e99c diff --git a/src/examples/ex02_root.ts b/src/examples/ex02_root.ts index 2bdaaadfd8..31dd48e818 100644 --- a/src/examples/ex02_root.ts +++ b/src/examples/ex02_root.ts @@ -1,6 +1,4 @@ -import { Field, Circuit, circuitMain, public_, isReady } from 'o1js'; - -await isReady; +import { Field, Circuit, circuitMain, public_, UInt64 } from 'o1js'; /* Exercise 2: @@ -11,9 +9,9 @@ Prove: class Main extends Circuit { @circuitMain - static main(y: Field, @public_ x: Field) { + static main(@public_ y: Field, x: UInt64) { let y3 = y.square().mul(y); - y3.assertEquals(x); + y3.assertEquals(x.value); } } @@ -24,15 +22,15 @@ console.timeEnd('generating keypair...'); console.log('prove...'); console.time('prove...'); -const x = new Field(8); +const x = UInt64.from(8); const y = new Field(2); -const proof = await Main.prove([y], [x], kp); +const proof = await Main.prove([x], [y], kp); console.timeEnd('prove...'); console.log('verify...'); console.time('verify...'); let vk = kp.verificationKey(); -let ok = await Main.verify([x], vk, proof); +let ok = await Main.verify([y], vk, proof); console.timeEnd('verify...'); console.log('ok?', ok); diff --git a/src/examples/ex02_root_program.ts b/src/examples/ex02_root_program.ts new file mode 100644 index 0000000000..c4115dbd8d --- /dev/null +++ b/src/examples/ex02_root_program.ts @@ -0,0 +1,42 @@ +import { + Field, + Circuit, + circuitMain, + public_, + UInt64, + Experimental, +} from 'o1js'; + +let { ZkProgram } = Experimental; + +const Main = ZkProgram({ + publicInput: Field, + methods: { + main: { + privateInputs: [UInt64], + method(y: Field, x: UInt64) { + let y3 = y.square().mul(y); + y3.assertEquals(x.value); + }, + }, + }, +}); + +console.log('generating keypair...'); +console.time('generating keypair...'); +const kp = await Main.compile(); +console.timeEnd('generating keypair...'); + +console.log('prove...'); +console.time('prove...'); +const x = UInt64.from(8); +const y = new Field(2); +const proof = await Main.main(y, x); +console.timeEnd('prove...'); + +console.log('verify...'); +console.time('verify...'); +let ok = await Main.verify(proof); +console.timeEnd('verify...'); + +console.log('ok?', ok); diff --git a/src/lib/gadgets/range-check.ts b/src/lib/gadgets/range-check.ts new file mode 100644 index 0000000000..12a914ccd8 --- /dev/null +++ b/src/lib/gadgets/range-check.ts @@ -0,0 +1,14 @@ +import { type Field } from '../field.js'; +import * as Gates from '../gates.js'; + +export { rangeCheck64 }; + +function rangeCheck64(x: Field) { + if (x.isConstant()) { + if (x.toBigInt() >= 1n << 64n) { + throw Error(`rangeCheck64: expected field to fit in 64 bits, got ${x}`); + } + } else { + Gates.rangeCheck64(x); + } +} diff --git a/src/lib/gates.ts b/src/lib/gates.ts new file mode 100644 index 0000000000..503c4d2c6a --- /dev/null +++ b/src/lib/gates.ts @@ -0,0 +1,49 @@ +import { Snarky } from '../snarky.js'; +import { FieldVar, FieldConst, type Field } from './field.js'; + +export { rangeCheck64 }; + +/** + * Asserts that x is at most 64 bits + */ +function rangeCheck64(x: Field) { + let [, x0, x2, x4, x6, x8, x10, x12, x14] = Snarky.exists(8, () => { + let xx = x.toBigInt(); + // crumbs (2-bit limbs) + return [ + 0, + getBits(xx, 0, 2), + getBits(xx, 2, 2), + getBits(xx, 4, 2), + getBits(xx, 6, 2), + getBits(xx, 8, 2), + getBits(xx, 10, 2), + getBits(xx, 12, 2), + getBits(xx, 14, 2), + ]; + }); + // 12-bit limbs + let [, x16, x28, x40, x52] = Snarky.exists(4, () => { + let xx = x.toBigInt(); + return [ + 0, + getBits(xx, 16, 12), + getBits(xx, 28, 12), + getBits(xx, 40, 12), + getBits(xx, 52, 12), + ]; + }); + Snarky.gates.rangeCheck0( + x.value, + [0, FieldVar[0], FieldVar[0], x52, x40, x28, x16], + [0, x14, x12, x10, x8, x6, x4, x2, x0], + // not using compact mode + FieldConst[0] + ); +} + +function getBits(x: bigint, start: number, length: number) { + return FieldConst.fromBigint( + (x >> BigInt(start)) & ((1n << BigInt(length)) - 1n) + ); +} diff --git a/src/lib/int.ts b/src/lib/int.ts index 39744a84cf..f4143a9a5b 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -69,9 +69,11 @@ class UInt64 extends CircuitValue { let actual = x.value.rangeCheckHelper(64); actual.assertEquals(x.value); } + static toInput(x: UInt64): HashInput { return { packed: [[x.value, 64]] }; } + /** * Encodes this structure into a JSON-like object. */ diff --git a/src/lib/ml/base.ts b/src/lib/ml/base.ts index cf68579ce0..1b6b7fd02a 100644 --- a/src/lib/ml/base.ts +++ b/src/lib/ml/base.ts @@ -77,5 +77,11 @@ const MlOption = Object.assign( if (option === undefined) return 0; return [0, map(option)]; }, + isNone(option: MlOption): option is 0 { + return option === 0; + }, + isSome(option: MlOption): option is [0, T] { + return option !== 0; + }, } ); diff --git a/src/lib/proof_system.ts b/src/lib/proof_system.ts index 36cb0e154f..553137d0fd 100644 --- a/src/lib/proof_system.ts +++ b/src/lib/proof_system.ts @@ -4,7 +4,14 @@ import { EmptyVoid, } from '../bindings/lib/generic.js'; import { withThreadPool } from '../bindings/js/wrapper.js'; -import { ProvablePure, Pickles } from '../snarky.js'; +import { + ProvablePure, + Pickles, + FeatureFlags, + MlFeatureFlags, + Gate, + GateType, +} from '../snarky.js'; import { Field, Bool } from './core.js'; import { FlexibleProvable, @@ -18,7 +25,7 @@ import { Provable } from './provable.js'; import { assert, prettifyStacktracePromise } from './errors.js'; import { snarkContext } from './provable-context.js'; import { hashConstant } from './hash.js'; -import { MlArray, MlTuple } from './ml/base.js'; +import { MlArray, MlBool, MlTuple } from './ml/base.js'; import { MlFieldArray, MlFieldConstArray } from './ml/fields.js'; import { FieldConst, FieldVar } from './field.js'; @@ -249,6 +256,12 @@ function ZkProgram< let methodFunctions = keys.map((key) => methods[key].method); let maxProofsVerified = getMaxProofsVerified(methodIntfs); + function analyzeMethods() { + return methodIntfs.map((methodEntry, i) => + analyzeMethod(publicInputType, methodEntry, methodFunctions[i]) + ); + } + let compileOutput: | { provers: Pickles.Prover[]; @@ -260,14 +273,17 @@ function ZkProgram< | undefined; async function compile() { - let { provers, verify, verificationKey } = await compileProgram( + let methodsMeta = analyzeMethods(); + let gates = methodsMeta.map((m) => m.gates); + let { provers, verify, verificationKey } = await compileProgram({ publicInputType, publicOutputType, methodIntfs, - methodFunctions, - selfTag, - config.overrideWrapDomain - ); + methods: methodFunctions, + gates, + proofSystemTag: selfTag, + overrideWrapDomain: config.overrideWrapDomain, + }); compileOutput = { provers, verify }; return { verificationKey: verificationKey.data }; } @@ -352,12 +368,6 @@ function ZkProgram< return hash.toBigInt().toString(16); } - function analyzeMethods() { - return methodIntfs.map((methodEntry, i) => - analyzeMethod(publicInputType, methodEntry, methodFunctions[i]) - ); - } - return Object.assign( selfTag, { @@ -500,21 +510,31 @@ type MethodInterface = { // reasonable default choice for `overrideWrapDomain` const maxProofsToWrapDomain = { 0: 0, 1: 1, 2: 1 } as const; -async function compileProgram( - publicInputType: ProvablePure, - publicOutputType: ProvablePure, - methodIntfs: MethodInterface[], - methods: ((...args: any) => void)[], - proofSystemTag: { name: string }, - overrideWrapDomain?: 0 | 1 | 2 -) { +async function compileProgram({ + publicInputType, + publicOutputType, + methodIntfs, + methods, + gates, + proofSystemTag, + overrideWrapDomain, +}: { + publicInputType: ProvablePure; + publicOutputType: ProvablePure; + methodIntfs: MethodInterface[]; + methods: ((...args: any) => void)[]; + gates: Gate[][]; + proofSystemTag: { name: string }; + overrideWrapDomain?: 0 | 1 | 2; +}) { let rules = methodIntfs.map((methodEntry, i) => picklesRuleFromFunction( publicInputType, publicOutputType, methods[i], proofSystemTag, - methodEntry + methodEntry, + gates[i] ) ); let maxProofs = getMaxProofsVerified(methodIntfs); @@ -589,7 +609,8 @@ function picklesRuleFromFunction( publicOutputType: ProvablePure, func: (...args: unknown[]) => any, proofSystemTag: { name: string }, - { methodName, witnessArgs, proofArgs, allArgs }: MethodInterface + { methodName, witnessArgs, proofArgs, allArgs }: MethodInterface, + gates: Gate[] ): Pickles.Rule { function main(publicInput: MlFieldArray): ReturnType { let { witnesses: argsWithoutPublicInput, inProver } = snarkContext.get(); @@ -664,9 +685,13 @@ function picklesRuleFromFunction( return { isSelf: false, tag: compiledTag }; } }); + + let featureFlags = computeFeatureFlags(gates); + return { identifier: methodName, main, + featureFlags, proofsToVerify: MlArray.to(proofsToVerify), }; } @@ -822,6 +847,46 @@ function dummyBase64Proof() { return withThreadPool(async () => Pickles.dummyBase64Proof()); } +// what feature flags to set to enable certain gate types + +const gateToFlag: Partial> = { + RangeCheck0: 'rangeCheck0', + RangeCheck1: 'rangeCheck1', + ForeignFieldAdd: 'foreignFieldAdd', + ForeignFieldMul: 'foreignFieldMul', + Xor16: 'xor', + Rot64: 'rot', + Lookup: 'lookup', +}; + +function computeFeatureFlags(gates: Gate[]): MlFeatureFlags { + let flags: FeatureFlags = { + rangeCheck0: false, + rangeCheck1: false, + foreignFieldAdd: false, + foreignFieldMul: false, + xor: false, + rot: false, + lookup: false, + runtimeTables: false, + }; + for (let gate of gates) { + let flag = gateToFlag[gate.type]; + if (flag !== undefined) flags[flag] = true; + } + return [ + 0, + MlBool(flags.rangeCheck0), + MlBool(flags.rangeCheck1), + MlBool(flags.foreignFieldAdd), + MlBool(flags.foreignFieldMul), + MlBool(flags.xor), + MlBool(flags.rot), + MlBool(flags.lookup), + MlBool(flags.runtimeTables), + ]; +} + // helpers for circuit context function Prover() { diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 50b1265441..cf91b86108 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -675,18 +675,20 @@ class SmartContract { }; }); // run methods once to get information that we need already at compile time - this.analyzeMethods(); + let methodsMeta = this.analyzeMethods(); + let gates = methodIntfs.map((intf) => methodsMeta[intf.methodName].gates); let { verificationKey: verificationKey_, provers, verify, - } = await compileProgram( - ZkappPublicInput, - Empty, + } = await compileProgram({ + publicInputType: ZkappPublicInput, + publicOutputType: Empty, methodIntfs, methods, - this - ); + gates, + proofSystemTag: this, + }); let verificationKey = { data: verificationKey_.data, hash: Field(verificationKey_.hash), diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 1843df88bc..f3e4d1e9e4 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -12,10 +12,18 @@ import type { } from './lib/ml/base.js'; import type { MlHashInput } from './lib/ml/conversion.js'; -export { ProvablePure, Provable, Ledger, Pickles, Gate }; +export { ProvablePure, Provable, Ledger, Pickles, Gate, GateType }; // internal -export { Snarky, Test, JsonGate, MlPublicKey, MlPublicKeyVar }; +export { + Snarky, + Test, + JsonGate, + MlPublicKey, + MlPublicKeyVar, + FeatureFlags, + MlFeatureFlags, +}; /** * `Provable` is the general circuit type interface in o1js. `Provable` interface describes how a type `T` is made up of {@link Field} elements and "auxiliary" (non-provable) data. @@ -273,6 +281,33 @@ declare const Snarky: { ]; }; + gates: { + /** + * Range check gate + * + * @param v0 field var to be range checked + * @param v0p bits 16 to 88 as 6 12-bit limbs + * @param v0c bits 0 to 16 as 8 2-bit limbs + * @param compact boolean field elements -- whether to use "compact mode" + */ + rangeCheck0( + v0: FieldVar, + v0p: [0, FieldVar, FieldVar, FieldVar, FieldVar, FieldVar, FieldVar], + v0c: [ + 0, + FieldVar, + FieldVar, + FieldVar, + FieldVar, + FieldVar, + FieldVar, + FieldVar, + FieldVar + ], + compact: FieldConst + ): void; + }; + bool: { not(x: BoolVar): BoolVar; @@ -357,15 +392,31 @@ declare const Snarky: { }; }; +type GateType = + | 'Zero' + | 'Generic' + | 'Poseidon' + | 'CompleteAdd' + | 'VarbaseMul' + | 'EndoMul' + | 'EndoMulScalar' + | 'Lookup' + | 'RangeCheck0' + | 'RangeCheck1' + | 'ForeignFieldAdd' + | 'ForeignFieldMul' + | 'Xor16' + | 'Rot64'; + type JsonGate = { - typ: string; + typ: GateType; wires: { row: number; col: number }[]; coeffs: string[]; }; type JsonConstraintSystem = { gates: JsonGate[]; public_input_size: number }; type Gate = { - type: string; + type: GateType; wires: { row: number; col: number }[]; coeffs: string[]; }; @@ -495,18 +546,56 @@ declare const Test: { }; }; +type FeatureFlags = { + rangeCheck0: boolean; + rangeCheck1: boolean; + foreignFieldAdd: boolean; + foreignFieldMul: boolean; + xor: boolean; + rot: boolean; + lookup: boolean; + runtimeTables: boolean; +}; + +type MlFeatureFlags = [ + _: 0, + rangeCheck0: MlBool, + rangeCheck1: MlBool, + foreignFieldAdd: MlBool, + foreignFieldMul: MlBool, + xor: MlBool, + rot: MlBool, + lookup: MlBool, + runtimeTables: MlBool +]; + declare namespace Pickles { type Proof = unknown; // opaque to js type Statement = [_: 0, publicInput: MlArray, publicOutput: MlArray]; + + /** + * A "rule" is a circuit plus some metadata for `Pickles.compile` + */ type Rule = { identifier: string; + /** + * The main circuit functions + */ main: (publicInput: MlArray) => { publicOutput: MlArray; previousStatements: MlArray>; shouldVerify: MlArray; }; + /** + * Feature flags which enable certain custom gates + */ + featureFlags: MlFeatureFlags; + /** + * Description of previous proofs to verify in this rule + */ proofsToVerify: MlArray<{ isSelf: true } | { isSelf: false; tag: unknown }>; }; + type Prover = ( publicInput: MlArray, previousProofs: MlArray