diff --git a/src/index.ts b/src/index.ts index 0cd002a58d..c6e6f78f04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,8 @@ export type { FlexibleProvablePure, InferProvable, } from './lib/provable/types/struct.js'; +export { From } from './bindings/lib/provable-generic.js'; +export { ProvableType } from './lib/provable/types/provable-intf.js'; export { provable, provablePure, @@ -79,17 +81,19 @@ export { state, State, declareState } from './lib/mina/state.js'; export type { JsonProof } from './lib/proof-system/zkprogram.js'; export { - type ProofBase, - Proof, - DynamicProof, SelfProof, verify, Empty, Undefined, Void, VerificationKey, - FeatureFlags, } from './lib/proof-system/zkprogram.js'; +export { + type ProofBase, + Proof, + DynamicProof, +} from './lib/proof-system/proof.js'; +export { FeatureFlags } from './lib/proof-system/feature-flags.js'; export { Cache, CacheHeader } from './lib/proof-system/cache.js'; export { Account } from './lib/mina/account.js'; @@ -104,7 +108,10 @@ export { } from './lib/mina/account-update.js'; export { TokenAccountUpdateIterator } from './lib/mina/token/forest-iterator.js'; -export { TokenContract, TokenContractV2 } from './lib/mina/token/token-contract.js'; +export { + TokenContract, + TokenContractV2, +} from './lib/mina/token/token-contract.js'; export type { TransactionStatus } from './lib/mina/graphql.js'; export { diff --git a/src/lib/mina/account-update.ts b/src/lib/mina/account-update.ts index f50c95da36..18cb08297b 100644 --- a/src/lib/mina/account-update.ts +++ b/src/lib/mina/account-update.ts @@ -33,12 +33,8 @@ import { ClosedInterval, getAccountPreconditions, } from './precondition.js'; -import { - dummyBase64Proof, - Empty, - Proof, - Prover, -} from '../proof-system/zkprogram.js'; +import { dummyBase64Proof, Empty, Prover } from '../proof-system/zkprogram.js'; +import { Proof } from '../proof-system/proof.js'; import { Memo } from '../../mina-signer/src/memo.js'; import { Events as BaseEvents, diff --git a/src/lib/mina/actions/batch-reducer.ts b/src/lib/mina/actions/batch-reducer.ts index c29cb040e1..21a701fc6a 100644 --- a/src/lib/mina/actions/batch-reducer.ts +++ b/src/lib/mina/actions/batch-reducer.ts @@ -1,4 +1,5 @@ -import { Proof, SelfProof } from '../../proof-system/zkprogram.js'; +import { SelfProof } from '../../proof-system/zkprogram.js'; +import { Proof } from '../../proof-system/proof.js'; import { Bool, Field } from '../../provable/wrapped.js'; import { SmartContract } from '../zkapp.js'; import { assert, assertDefined } from '../../util/assert.js'; diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index d69595ad7b..98c63a8670 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -1,4 +1,5 @@ -import { Proof, ZkProgram } from '../../proof-system/zkprogram.js'; +import { ZkProgram } from '../../proof-system/zkprogram.js'; +import { Proof } from '../../proof-system/proof.js'; import { Bool, Field } from '../../provable/wrapped.js'; import { MerkleList, MerkleListIterator } from '../../provable/merkle-list.js'; import { Actions } from '../../../bindings/mina-transaction/transaction-leaves.js'; diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 090b7d3496..8f5193f7ad 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -8,7 +8,7 @@ import { toKeyHash, } from './offchain-state-serialization.js'; import { Field } from '../../provable/wrapped.js'; -import { Proof } from '../../proof-system/zkprogram.js'; +import { Proof } from '../../proof-system/proof.js'; import { OffchainStateCommitments, OffchainStateRollup, diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index 0c99d47e6f..82fc5e3ce0 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -11,7 +11,8 @@ import { import { Field } from '../provable/wrapped.js'; import { PrivateKey, PublicKey } from '../provable/crypto/signature.js'; import { UInt32, UInt64 } from '../provable/int.js'; -import { Empty, Proof } from '../proof-system/zkprogram.js'; +import { Empty } from '../proof-system/zkprogram.js'; +import { Proof } from '../proof-system/proof.js'; import { currentTransaction } from './transaction-context.js'; import { Provable } from '../provable/provable.js'; import { assertPreconditionInvariants } from './precondition.js'; diff --git a/src/lib/mina/zkapp.ts b/src/lib/mina/zkapp.ts index 09409367a1..fd4a8397fb 100644 --- a/src/lib/mina/zkapp.ts +++ b/src/lib/mina/zkapp.ts @@ -49,9 +49,9 @@ import { methodArgumentsToConstant, methodArgumentTypesAndValues, MethodInterface, - Proof, sortMethodArguments, } from '../proof-system/zkprogram.js'; +import { Proof } from '../proof-system/proof.js'; import { PublicKey } from '../provable/crypto/signature.js'; import { InternalStateType, @@ -1107,16 +1107,20 @@ super.init(); } const queryFilterOptions: EventActionFilterOptions = {}; - if(start.greaterThan(UInt32.from(0)).toBoolean()) { + if (start.greaterThan(UInt32.from(0)).toBoolean()) { queryFilterOptions.from = start; } - if(end) { + if (end) { queryFilterOptions.to = end; } // filters all elements so that they are within the given range // only returns { type: "", event: [] } in a flat format let events = ( - await Mina.fetchEvents(this.address, this.self.body.tokenId, queryFilterOptions) + await Mina.fetchEvents( + this.address, + this.self.body.tokenId, + queryFilterOptions + ) ) .map((event) => { return event.events.map((eventData) => { diff --git a/src/lib/proof-system/feature-flags.ts b/src/lib/proof-system/feature-flags.ts new file mode 100644 index 0000000000..1f5c80a0d5 --- /dev/null +++ b/src/lib/proof-system/feature-flags.ts @@ -0,0 +1,189 @@ +import { MlFeatureFlags, Gate, GateType } from '../../snarky.js'; +import { MlBool, MlOption, MlArrayOptionalElements } from '../ml/base.js'; +import type { analyzeMethod } from './zkprogram.js'; + +// public API +export { FeatureFlags }; + +// internal API +export { featureFlagsToMlOption, featureFlagsFromGates }; + +type AnalysableProgram = { + analyzeMethods: () => Promise<{ + [I in keyof any]: Awaited>; + }>; +}; + +type FeatureFlags = { + rangeCheck0: boolean | undefined; + rangeCheck1: boolean | undefined; + foreignFieldAdd: boolean | undefined; + foreignFieldMul: boolean | undefined; + xor: boolean | undefined; + rot: boolean | undefined; + lookup: boolean | undefined; + runtimeTables: boolean | undefined; +}; +/** + * Feature flags indicate what custom gates are used in a proof of circuit. + * Side loading, for example, requires a set of feature flags in advance (at compile time) in order to verify and side load proofs. + * If the side loaded proofs and verification keys do not match the specified feature flag configurations, the verification will fail. + * Flags specified as `undefined` are considered as `maybe` by Pickles. This means, proofs can be sided loaded that can, but don't have to, use a specific custom gate. + * _Note:_ `Maybe` feature flags incur a proving overhead. + */ +const FeatureFlags = { + /** + * Returns a feature flag configuration where all flags are set to false. + */ + allNone: { + rangeCheck0: false, + rangeCheck1: false, + foreignFieldAdd: false, + foreignFieldMul: false, + xor: false, + rot: false, + lookup: false, + runtimeTables: false, + }, + /** + * Returns a feature flag configuration where all flags are optional. + */ + allMaybe: { + rangeCheck0: undefined, + rangeCheck1: undefined, + foreignFieldAdd: undefined, + foreignFieldMul: undefined, + xor: undefined, + rot: undefined, + lookup: undefined, + runtimeTables: undefined, + }, + + /** + * Given a list of gates, returns the feature flag configuration that the gates use. + */ + fromGates: featureFlagsFromGates, + + /** + * Given a ZkProgram, return the feature flag configuration that fits the given program. + * This function considers all methods of the specified ZkProgram and finds a configuration that fits all. + */ + fromZkProgram: async (program: AnalysableProgram) => + await fromZkProgramList([program]), + + /** + * Given a list of ZkPrograms, return the feature flag configuration that fits the given set of programs. + * This function considers all methods of all specified ZkPrograms and finds a configuration that fits all. + */ + fromZkProgramList, +}; + +async function fromZkProgramList(programs: Array) { + let flatMethodIntfs: Array>> = []; + for (const program of programs) { + let methodInterface = await program.analyzeMethods(); + flatMethodIntfs.push(...Object.values(methodInterface)); + } + + return featureFlagsfromFlatMethodIntfs(flatMethodIntfs); +} + +async function featureFlagsfromFlatMethodIntfs( + methodIntfs: Array>> +): Promise { + // compute feature flags that belong to each method + let flags = methodIntfs.map(({ gates }) => { + return featureFlagsFromGates(gates); + }); + if (flags.length === 0) + throw Error( + 'The ZkProgram has no methods, in order to calculate feature flags, please attach a method to your ZkProgram.' + ); + + // initialize feature flags to all false + let globalFlags: Record = { + rangeCheck0: false, + rangeCheck1: false, + foreignFieldAdd: false, + foreignFieldMul: false, + xor: false, + rot: false, + lookup: false, + runtimeTables: false, + }; + + // if there's only one method that means it defines the feature flags for the entire program + if (flags.length === 1) return flags[0]; + + // calculating the crossover between all methods, compute the shared feature flag set + flags.forEach((featureFlags, i) => { + for (const [flagType, currentFlag] of Object.entries(featureFlags)) { + if (i === 0) { + // initialize first iteration of flags freely + globalFlags[flagType] = currentFlag; + } else if (globalFlags[flagType] != currentFlag) { + // if flags conflict, set them to undefined to account for both cases (true and false) ^= maybe + // otherwise side loading couldn't verify some proofs of some method branches! + globalFlags[flagType] = undefined; + } + } + }); + return globalFlags as FeatureFlags; +} + +// 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 featureFlagsFromGates(gates: Gate[]): FeatureFlags { + 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 flags; +} + +function featureFlagsToMlOption( + flags: FeatureFlags +): MlArrayOptionalElements { + const { + rangeCheck0, + rangeCheck1, + foreignFieldAdd, + foreignFieldMul, + xor, + rot, + lookup, + runtimeTables, + } = flags; + + return [ + 0, + MlOption.mapTo(rangeCheck0, MlBool), + MlOption.mapTo(rangeCheck1, MlBool), + MlOption.mapTo(foreignFieldAdd, MlBool), + MlOption.mapTo(foreignFieldMul, MlBool), + MlOption.mapTo(xor, MlBool), + MlOption.mapTo(rot, MlBool), + MlOption.mapTo(lookup, MlBool), + MlOption.mapTo(runtimeTables, MlBool), + ]; +} diff --git a/src/lib/proof-system/proof-system.unit-test.ts b/src/lib/proof-system/proof-system.unit-test.ts index b2e7501f94..44953c441d 100644 --- a/src/lib/proof-system/proof-system.unit-test.ts +++ b/src/lib/proof-system/proof-system.unit-test.ts @@ -4,11 +4,11 @@ import { UInt64 } from '../provable/int.js'; import { CompiledTag, Empty, - Proof, ZkProgram, picklesRuleFromFunction, sortMethodArguments, } from './zkprogram.js'; +import { Proof } from './proof.js'; import { expect } from 'expect'; import { Pickles, Snarky } from '../../snarky.js'; import { AnyFunction } from '../util/types.js'; diff --git a/src/lib/proof-system/proof.ts b/src/lib/proof-system/proof.ts new file mode 100644 index 0000000000..af8a0455ca --- /dev/null +++ b/src/lib/proof-system/proof.ts @@ -0,0 +1,304 @@ +import { initializeBindings, withThreadPool } from '../../snarky.js'; +import { Pickles } from '../../snarky.js'; +import { Field, Bool } from '../provable/wrapped.js'; +import { + FlexibleProvablePure, + InferProvable, +} from '../provable/types/struct.js'; +import { ProvablePure } from '../provable/types/provable-intf.js'; +import { FeatureFlags } from './feature-flags.js'; +import type { VerificationKey, JsonProof } from './zkprogram.js'; +import { Subclass } from '../util/types.js'; + +// public API +export { ProofBase, Proof, DynamicProof }; + +// internal API +export { dummyProof, getStatementType }; + +class ProofBase { + static publicInputType: FlexibleProvablePure = undefined as any; + static publicOutputType: FlexibleProvablePure = undefined as any; + static tag: () => { name: string } = () => { + throw Error( + `You cannot use the \`Proof\` class directly. Instead, define a subclass:\n` + + `class MyProof extends Proof { ... }` + ); + }; + publicInput: Input; + publicOutput: Output; + proof: Pickles.Proof; + maxProofsVerified: 0 | 1 | 2; + shouldVerify = Bool(false); + + toJSON(): JsonProof { + let type = getStatementType(this.constructor as any); + return { + publicInput: type.input.toFields(this.publicInput).map(String), + publicOutput: type.output.toFields(this.publicOutput).map(String), + maxProofsVerified: this.maxProofsVerified, + proof: Pickles.proofToBase64([this.maxProofsVerified, this.proof]), + }; + } + + constructor({ + proof, + publicInput, + publicOutput, + maxProofsVerified, + }: { + proof: Pickles.Proof; + publicInput: Input; + publicOutput: Output; + maxProofsVerified: 0 | 1 | 2; + }) { + this.publicInput = publicInput; + this.publicOutput = publicOutput; + this.proof = proof; // TODO optionally convert from string? + this.maxProofsVerified = maxProofsVerified; + } +} + +class Proof extends ProofBase { + verify() { + this.shouldVerify = Bool(true); + } + verifyIf(condition: Bool) { + this.shouldVerify = condition; + } + + static async fromJSON>( + this: S, + { + maxProofsVerified, + proof: proofString, + publicInput: publicInputJson, + publicOutput: publicOutputJson, + }: JsonProof + ): Promise< + Proof< + InferProvable, + InferProvable + > + > { + await initializeBindings(); + let [, proof] = Pickles.proofOfBase64(proofString, maxProofsVerified); + let type = getStatementType(this); + let publicInput = type.input.fromFields(publicInputJson.map(Field)); + let publicOutput = type.output.fromFields(publicOutputJson.map(Field)); + return new this({ + publicInput, + publicOutput, + proof, + maxProofsVerified, + }) as any; + } + + /** + * Dummy proof. This can be useful for ZkPrograms that handle the base case in the same + * method as the inductive case, using a pattern like this: + * + * ```ts + * method(proof: SelfProof, isRecursive: Bool) { + * proof.verifyIf(isRecursive); + * // ... + * } + * ``` + * + * To use such a method in the base case, you need a dummy proof: + * + * ```ts + * let dummy = await MyProof.dummy(publicInput, publicOutput, 1); + * await myProgram.myMethod(dummy, Bool(false)); + * ``` + * + * **Note**: The types of `publicInput` and `publicOutput`, as well as the `maxProofsVerified` parameter, + * must match your ZkProgram. `maxProofsVerified` is the maximum number of proofs that any of your methods take as arguments. + */ + static async dummy( + publicInput: Input, + publicOutput: OutPut, + maxProofsVerified: 0 | 1 | 2, + domainLog2: number = 14 + ): Promise> { + let dummyRaw = await dummyProof(maxProofsVerified, domainLog2); + return new this({ + publicInput, + publicOutput, + proof: dummyRaw, + maxProofsVerified, + }); + } +} + +var sideloadedKeysCounter = 0; + +/** + * The `DynamicProof` class enables circuits to verify proofs using in-ciruit verfication keys. + * This is opposed to the baked-in verification keys of the `Proof` class. + * + * In order to use this, a subclass of DynamicProof that specifies the public input and output types along with the maxProofsVerified number has to be created. + * + * ```ts + * export class SideloadedProgramProof extends DynamicProof { + * static publicInputType = MyStruct; + * static publicOutputType = Field; + * static maxProofsVerified = 0 as const; + * } + * ``` + * + * The `maxProofsVerified` constant is a product of the child circuit and indicates the maximum number that that circuit verifies itself. + * If you are unsure about what that is for you, you should use `2`. + * + * Any `DynamicProof` subclass can be used as private input to ZkPrograms or SmartContracts along with a `VerificationKey` input. + * ```ts + * proof.verify(verificationKey) + * ``` + * + * NOTE: In the case of `DynamicProof`s, the circuit makes no assertions about the verificationKey used on its own. + * This is the responsibility of the application developer and should always implement appropriate checks. + * This pattern differs a lot from the usage of normal `Proof`, where the verification key is baked into the compiled circuit. + * @see {@link src/examples/zkprogram/dynamic-keys-merkletree.ts} for an example of how this can be done using merkle trees + * + * Assertions generally only happen using the vk hash that is part of the `VerificationKey` struct along with the raw vk data as auxiliary data. + * When using verify() on a `DynamicProof`, Pickles makes sure that the verification key data matches the hash. + * Therefore all manual assertions have to be made on the vk's hash and it can be assumed that the vk's data is checked to match the hash if it is used with verify(). + */ +class DynamicProof extends ProofBase { + public static maxProofsVerified: 0 | 1 | 2; + + private static memoizedCounter: number | undefined; + + /** + * As the name indicates, feature flags are features of the proof system. + * + * If we want to side load proofs and verification keys, we first have to tell Pickles what _shape_ of proofs it should expect. + * + * For example, if we want to side load proofs that use foreign field arithmetic custom gates, we have to make Pickles aware of that by defining + * these custom gates. + * + * _Note:_ Only proofs that use the exact same composition of custom gates which were expected by Pickles can be verified using side loading. + * If you want to verify _any_ proof, no matter what custom gates it uses, you can use {@link FeatureFlags.allMaybe}. Please note that this might incur a significant overhead. + * + * You can also toggle specific feature flags manually by specifying them here. + * Alternatively, you can use {@FeatureFlags.fromZkProgram} to compute the set of feature flags that are compatible with a given program. + */ + static featureFlags: FeatureFlags = FeatureFlags.allNone; + + static tag() { + let counter: number; + if (this.memoizedCounter !== undefined) { + counter = this.memoizedCounter; + } else { + counter = sideloadedKeysCounter++; + this.memoizedCounter = counter; + } + return { name: `o1js-sideloaded-${counter}` }; + } + + usedVerificationKey?: VerificationKey; + + /** + * Verifies this DynamicProof using a given verification key + * @param vk The verification key this proof will be verified against + */ + verify(vk: VerificationKey) { + this.shouldVerify = Bool(true); + this.usedVerificationKey = vk; + } + verifyIf(vk: VerificationKey, condition: Bool) { + this.shouldVerify = condition; + this.usedVerificationKey = vk; + } + + static async fromJSON>( + this: S, + { + maxProofsVerified, + proof: proofString, + publicInput: publicInputJson, + publicOutput: publicOutputJson, + }: JsonProof + ): Promise< + DynamicProof< + InferProvable, + InferProvable + > + > { + await initializeBindings(); + let [, proof] = Pickles.proofOfBase64(proofString, maxProofsVerified); + let type = getStatementType(this); + let publicInput = type.input.fromFields(publicInputJson.map(Field)); + let publicOutput = type.output.fromFields(publicOutputJson.map(Field)); + return new this({ + publicInput, + publicOutput, + proof, + maxProofsVerified, + }) as any; + } + + static async dummy>( + this: S, + publicInput: InferProvable, + publicOutput: InferProvable, + maxProofsVerified: 0 | 1 | 2, + domainLog2: number = 14 + ): Promise> { + return this.fromProof( + await Proof.dummy< + InferProvable, + InferProvable + >(publicInput, publicOutput, maxProofsVerified, domainLog2) + ); + } + + /** + * Converts a Proof into a DynamicProof carrying over all relevant data. + * This method can be used to convert a Proof computed by a ZkProgram + * into a DynamicProof that is accepted in a circuit that accepts DynamicProofs + */ + static fromProof>( + this: S, + proof: Proof< + InferProvable, + InferProvable + > + ): InstanceType { + return new this({ + publicInput: proof.publicInput, + publicOutput: proof.publicOutput, + maxProofsVerified: proof.maxProofsVerified, + proof: proof.proof, + }) as InstanceType; + } +} + +async function dummyProof(maxProofsVerified: 0 | 1 | 2, domainLog2: number) { + await initializeBindings(); + return withThreadPool( + async () => Pickles.dummyProof(maxProofsVerified, domainLog2)[1] + ); +} + +// helper + +function getStatementType< + T, + O, + P extends Subclass = typeof ProofBase +>(Proof: P): { input: ProvablePure; output: ProvablePure } { + if ( + Proof.publicInputType === undefined || + Proof.publicOutputType === undefined + ) { + throw Error( + `You cannot use the \`Proof\` class directly. Instead, define a subclass:\n` + + `class MyProof extends Proof { ... }` + ); + } + return { + input: Proof.publicInputType as any, + output: Proof.publicOutputType as any, + }; +} diff --git a/src/lib/proof-system/sideloaded.unit-test.ts b/src/lib/proof-system/sideloaded.unit-test.ts index 88bb40fca8..98608d1542 100644 --- a/src/lib/proof-system/sideloaded.unit-test.ts +++ b/src/lib/proof-system/sideloaded.unit-test.ts @@ -1,12 +1,7 @@ -import { - DynamicProof, - Proof, - VerificationKey, - Void, - ZkProgram, -} from './zkprogram.js'; +import { VerificationKey, Void, ZkProgram } from './zkprogram.js'; +import { DynamicProof } from './proof.js'; import { Field, SmartContract, Struct, method } from '../../index.js'; -import { it, describe, before } from 'node:test'; +import { it, describe } from 'node:test'; import { expect } from 'expect'; const program1 = ZkProgram({ diff --git a/src/lib/proof-system/zkprogram.ts b/src/lib/proof-system/zkprogram.ts index cfc71ee34c..aa00bcb1ad 100644 --- a/src/lib/proof-system/zkprogram.ts +++ b/src/lib/proof-system/zkprogram.ts @@ -1,6 +1,6 @@ import { EmptyUndefined, EmptyVoid } from '../../bindings/lib/generic.js'; import { Snarky, initializeBindings, withThreadPool } from '../../snarky.js'; -import { Pickles, MlFeatureFlags, Gate, GateType } from '../../snarky.js'; +import { Pickles, Gate } from '../../snarky.js'; import { Field, Bool } from '../provable/wrapped.js'; import { FlexibleProvable, @@ -18,14 +18,7 @@ import { Provable } from '../provable/provable.js'; import { assert, prettifyStacktracePromise } from '../util/errors.js'; import { snarkContext } from '../provable/core/provable-context.js'; import { hashConstant } from '../provable/crypto/poseidon.js'; -import { - MlArray, - MlBool, - MlResult, - MlPair, - MlOption, - MlArrayOptionalElements, -} from '../ml/base.js'; +import { MlArray, MlBool, MlResult, MlPair } from '../ml/base.js'; import { MlFieldArray, MlFieldConstArray } from '../ml/fields.js'; import { FieldVar, FieldConst } from '../provable/core/fieldvar.js'; import { Cache, readCache, writeCache } from './cache.js'; @@ -46,12 +39,21 @@ import { } from '../provable/types/provable-intf.js'; import { prefixToField } from '../../bindings/lib/binable.js'; import { prefixes } from '../../bindings/crypto/constants.js'; +import { Subclass, Tuple } from '../util/types.js'; +import { + dummyProof, + DynamicProof, + getStatementType, + Proof, + ProofBase, +} from './proof.js'; +import { + featureFlagsFromGates, + featureFlagsToMlOption, +} from './feature-flags.js'; // public API export { - ProofBase, - Proof, - DynamicProof, SelfProof, JsonProof, ZkProgram, @@ -60,7 +62,6 @@ export { Undefined, Void, VerificationKey, - FeatureFlags, }; // internal API @@ -90,388 +91,6 @@ const Empty = Undefined; type Void = undefined; const Void: ProvablePureExtended = EmptyVoid(); -type AnalysableProgram = { - analyzeMethods: () => Promise<{ - [I in keyof any]: UnwrapPromise>; - }>; -}; - -type FeatureFlags = { - rangeCheck0: boolean | undefined; - rangeCheck1: boolean | undefined; - foreignFieldAdd: boolean | undefined; - foreignFieldMul: boolean | undefined; - xor: boolean | undefined; - rot: boolean | undefined; - lookup: boolean | undefined; - runtimeTables: boolean | undefined; -}; -/** - * Feature flags indicate what custom gates are used in a proof of circuit. - * Side loading, for example, requires a set of feature flags in advance (at compile time) in order to verify and side load proofs. - * If the side loaded proofs and verification keys do not match the specified feature flag configurations, the verification will fail. - * Flags specified as `undefined` are considered as `maybe` by Pickles. This means, proofs can be sided loaded that can, but don't have to, use a specific custom gate. - * _Note:_ `Maybe` feature flags incur a proving overhead. - */ -const FeatureFlags = { - /** - * Returns a feature flag configuration where all flags are set to false. - */ - allNone: { - rangeCheck0: false, - rangeCheck1: false, - foreignFieldAdd: false, - foreignFieldMul: false, - xor: false, - rot: false, - lookup: false, - runtimeTables: false, - }, - /** - * Returns a feature flag configuration where all flags are optional. - */ - allMaybe: { - rangeCheck0: undefined, - rangeCheck1: undefined, - foreignFieldAdd: undefined, - foreignFieldMul: undefined, - xor: undefined, - rot: undefined, - lookup: undefined, - runtimeTables: undefined, - }, - - /** - * Given a list of gates, returns the feature flag configuration that the gates use. - */ - fromGates: featureFlagsFromGates, - - /** - * Given a ZkProgram, return the feature flag configuration that fits the given program. - * This function considers all methods of the specified ZkProgram and finds a configuration that fits all. - */ - fromZkProgram: async (program: AnalysableProgram) => - await fromZkProgramList([program]), - - /** - * Given a list of ZkPrograms, return the feature flag configuration that fits the given set of programs. - * This function considers all methods of all specified ZkPrograms and finds a configuration that fits all. - */ - fromZkProgramList, -}; - -async function fromZkProgramList(programs: Array) { - let flatMethodIntfs: Array>> = - []; - for (const program of programs) { - let methodInterface = await program.analyzeMethods(); - flatMethodIntfs.push(...Object.values(methodInterface)); - } - - return featureFlagsfromFlatMethodIntfs(flatMethodIntfs); -} - -async function featureFlagsfromFlatMethodIntfs( - methodIntfs: Array>> -): Promise { - // compute feature flags that belong to each method - let flags = methodIntfs.map(({ gates }) => { - return featureFlagsFromGates(gates); - }); - if (flags.length === 0) - throw Error( - 'The ZkProgram has no methods, in order to calculate feature flags, please attach a method to your ZkProgram.' - ); - - // initialize feature flags to all false - let globalFlags: Record = { - rangeCheck0: false, - rangeCheck1: false, - foreignFieldAdd: false, - foreignFieldMul: false, - xor: false, - rot: false, - lookup: false, - runtimeTables: false, - }; - - // if there's only one method that means it defines the feature flags for the entire program - if (flags.length === 1) return flags[0]; - - // calculating the crossover between all methods, compute the shared feature flag set - flags.forEach((featureFlags, i) => { - for (const [flagType, currentFlag] of Object.entries(featureFlags)) { - if (i === 0) { - // initialize first iteration of flags freely - globalFlags[flagType] = currentFlag; - } else if (globalFlags[flagType] != currentFlag) { - // if flags conflict, set them to undefined to account for both cases (true and false) ^= maybe - // otherwise side loading couldn't verify some proofs of some method branches! - globalFlags[flagType] = undefined; - } - } - }); - return globalFlags as FeatureFlags; -} - -class ProofBase { - static publicInputType: FlexibleProvablePure = undefined as any; - static publicOutputType: FlexibleProvablePure = undefined as any; - static tag: () => { name: string } = () => { - throw Error( - `You cannot use the \`Proof\` class directly. Instead, define a subclass:\n` + - `class MyProof extends Proof { ... }` - ); - }; - publicInput: Input; - publicOutput: Output; - proof: Pickles.Proof; - maxProofsVerified: 0 | 1 | 2; - shouldVerify = Bool(false); - - toJSON(): JsonProof { - let type = getStatementType(this.constructor as any); - return { - publicInput: type.input.toFields(this.publicInput).map(String), - publicOutput: type.output.toFields(this.publicOutput).map(String), - maxProofsVerified: this.maxProofsVerified, - proof: Pickles.proofToBase64([this.maxProofsVerified, this.proof]), - }; - } - - constructor({ - proof, - publicInput, - publicOutput, - maxProofsVerified, - }: { - proof: Pickles.Proof; - publicInput: Input; - publicOutput: Output; - maxProofsVerified: 0 | 1 | 2; - }) { - this.publicInput = publicInput; - this.publicOutput = publicOutput; - this.proof = proof; // TODO optionally convert from string? - this.maxProofsVerified = maxProofsVerified; - } -} - -class Proof extends ProofBase { - verify() { - this.shouldVerify = Bool(true); - } - verifyIf(condition: Bool) { - this.shouldVerify = condition; - } - - static async fromJSON>( - this: S, - { - maxProofsVerified, - proof: proofString, - publicInput: publicInputJson, - publicOutput: publicOutputJson, - }: JsonProof - ): Promise< - Proof< - InferProvable, - InferProvable - > - > { - await initializeBindings(); - let [, proof] = Pickles.proofOfBase64(proofString, maxProofsVerified); - let type = getStatementType(this); - let publicInput = type.input.fromFields(publicInputJson.map(Field)); - let publicOutput = type.output.fromFields(publicOutputJson.map(Field)); - return new this({ - publicInput, - publicOutput, - proof, - maxProofsVerified, - }) as any; - } - - /** - * Dummy proof. This can be useful for ZkPrograms that handle the base case in the same - * method as the inductive case, using a pattern like this: - * - * ```ts - * method(proof: SelfProof, isRecursive: Bool) { - * proof.verifyIf(isRecursive); - * // ... - * } - * ``` - * - * To use such a method in the base case, you need a dummy proof: - * - * ```ts - * let dummy = await MyProof.dummy(publicInput, publicOutput, 1); - * await myProgram.myMethod(dummy, Bool(false)); - * ``` - * - * **Note**: The types of `publicInput` and `publicOutput`, as well as the `maxProofsVerified` parameter, - * must match your ZkProgram. `maxProofsVerified` is the maximum number of proofs that any of your methods take as arguments. - */ - static async dummy( - publicInput: Input, - publicOutput: OutPut, - maxProofsVerified: 0 | 1 | 2, - domainLog2: number = 14 - ): Promise> { - let dummyRaw = await dummyProof(maxProofsVerified, domainLog2); - return new this({ - publicInput, - publicOutput, - proof: dummyRaw, - maxProofsVerified, - }); - } -} - -var sideloadedKeysCounter = 0; - -/** - * The `DynamicProof` class enables circuits to verify proofs using in-ciruit verfication keys. - * This is opposed to the baked-in verification keys of the `Proof` class. - * - * In order to use this, a subclass of DynamicProof that specifies the public input and output types along with the maxProofsVerified number has to be created. - * - * ```ts - * export class SideloadedProgramProof extends DynamicProof { - * static publicInputType = MyStruct; - * static publicOutputType = Field; - * static maxProofsVerified = 0 as const; - * } - * ``` - * - * The `maxProofsVerified` constant is a product of the child circuit and indicates the maximum number that that circuit verifies itself. - * If you are unsure about what that is for you, you should use `2`. - * - * Any `DynamicProof` subclass can be used as private input to ZkPrograms or SmartContracts along with a `VerificationKey` input. - * ```ts - * proof.verify(verificationKey) - * ``` - * - * NOTE: In the case of `DynamicProof`s, the circuit makes no assertions about the verificationKey used on its own. - * This is the responsibility of the application developer and should always implement appropriate checks. - * This pattern differs a lot from the usage of normal `Proof`, where the verification key is baked into the compiled circuit. - * @see {@link src/examples/zkprogram/dynamic-keys-merkletree.ts} for an example of how this can be done using merkle trees - * - * Assertions generally only happen using the vk hash that is part of the `VerificationKey` struct along with the raw vk data as auxiliary data. - * When using verify() on a `DynamicProof`, Pickles makes sure that the verification key data matches the hash. - * Therefore all manual assertions have to be made on the vk's hash and it can be assumed that the vk's data is checked to match the hash if it is used with verify(). - */ -class DynamicProof extends ProofBase { - public static maxProofsVerified: 0 | 1 | 2; - - private static memoizedCounter: number | undefined; - - /** - * As the name indicates, feature flags are features of the proof system. - * - * If we want to side load proofs and verification keys, we first have to tell Pickles what _shape_ of proofs it should expect. - * - * For example, if we want to side load proofs that use foreign field arithmetic custom gates, we have to make Pickles aware of that by defining - * these custom gates. - * - * _Note:_ Only proofs that use the exact same composition of custom gates which were expected by Pickles can be verified using side loading. - * If you want to verify _any_ proof, no matter what custom gates it uses, you can use {@link FeatureFlags.allMaybe}. Please note that this might incur a significant overhead. - * - * You can also toggle specific feature flags manually by specifying them here. - * Alternatively, you can use {@FeatureFlags.fromZkProgram} to compute the set of feature flags that are compatible with a given program. - */ - static featureFlags: FeatureFlags = FeatureFlags.allNone; - - static tag() { - let counter: number; - if (this.memoizedCounter !== undefined) { - counter = this.memoizedCounter; - } else { - counter = sideloadedKeysCounter++; - this.memoizedCounter = counter; - } - return { name: `o1js-sideloaded-${counter}` }; - } - - usedVerificationKey?: VerificationKey; - - /** - * Verifies this DynamicProof using a given verification key - * @param vk The verification key this proof will be verified against - */ - verify(vk: VerificationKey) { - this.shouldVerify = Bool(true); - this.usedVerificationKey = vk; - } - verifyIf(vk: VerificationKey, condition: Bool) { - this.shouldVerify = condition; - this.usedVerificationKey = vk; - } - - static async fromJSON>( - this: S, - { - maxProofsVerified, - proof: proofString, - publicInput: publicInputJson, - publicOutput: publicOutputJson, - }: JsonProof - ): Promise< - DynamicProof< - InferProvable, - InferProvable - > - > { - await initializeBindings(); - let [, proof] = Pickles.proofOfBase64(proofString, maxProofsVerified); - let type = getStatementType(this); - let publicInput = type.input.fromFields(publicInputJson.map(Field)); - let publicOutput = type.output.fromFields(publicOutputJson.map(Field)); - return new this({ - publicInput, - publicOutput, - proof, - maxProofsVerified, - }) as any; - } - - static async dummy>( - this: S, - publicInput: InferProvable, - publicOutput: InferProvable, - maxProofsVerified: 0 | 1 | 2, - domainLog2: number = 14 - ): Promise> { - return this.fromProof( - await Proof.dummy< - InferProvable, - InferProvable - >(publicInput, publicOutput, maxProofsVerified, domainLog2) - ); - } - - /** - * Converts a Proof into a DynamicProof carrying over all relevant data. - * This method can be used to convert a Proof computed by a ZkProgram - * into a DynamicProof that is accepted in a circuit that accepts DynamicProofs - */ - static fromProof>( - this: S, - proof: Proof< - InferProvable, - InferProvable - > - ): InstanceType { - return new this({ - publicInput: proof.publicInput, - publicOutput: proof.publicOutput, - maxProofsVerified: proof.maxProofsVerified, - proof: proof.proof, - }) as InstanceType; - } -} - async function verify( proof: ProofBase | JsonProof, verificationKey: string | VerificationKey @@ -1331,26 +950,6 @@ function emptyWitness(type: Provable) { return Provable.witness(type, () => emptyValue(type)); } -function getStatementType< - T, - O, - P extends Subclass = typeof ProofBase ->(Proof: P): { input: ProvablePure; output: ProvablePure } { - if ( - Proof.publicInputType === undefined || - Proof.publicOutputType === undefined - ) { - throw Error( - `You cannot use the \`Proof\` class directly. Instead, define a subclass:\n` + - `class MyProof extends Proof { ... }` - ); - } - return { - input: Proof.publicInputType as any, - output: Proof.publicOutputType as any, - }; -} - function getMaxProofsVerified(methodIntfs: MethodInterface[]) { return methodIntfs.reduce( (acc, { proofArgs }) => Math.max(acc, proofArgs.length), @@ -1389,13 +988,6 @@ ZkProgram.Proof = function < }; }; -async function dummyProof(maxProofsVerified: 0 | 1 | 2, domainLog2: number) { - await initializeBindings(); - return withThreadPool( - async () => Pickles.dummyProof(maxProofsVerified, domainLog2)[1] - ); -} - let dummyProofCache: string | undefined; async function dummyBase64Proof() { @@ -1406,63 +998,6 @@ async function dummyBase64Proof() { return base64Proof; } -// 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 featureFlagsFromGates(gates: Gate[]): FeatureFlags { - 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 flags; -} - -function featureFlagsToMlOption( - flags: FeatureFlags -): MlArrayOptionalElements { - const { - rangeCheck0, - rangeCheck1, - foreignFieldAdd, - foreignFieldMul, - xor, - rot, - lookup, - runtimeTables, - } = flags; - - return [ - 0, - MlOption.mapTo(rangeCheck0, MlBool), - MlOption.mapTo(rangeCheck1, MlBool), - MlOption.mapTo(foreignFieldAdd, MlBool), - MlOption.mapTo(foreignFieldMul, MlBool), - MlOption.mapTo(xor, MlBool), - MlOption.mapTo(rot, MlBool), - MlOption.mapTo(lookup, MlBool), - MlOption.mapTo(runtimeTables, MlBool), - ]; -} - // helpers for circuit context function Prover() { @@ -1493,17 +1028,10 @@ type Infer = T extends Subclass ? InferProvableType : never; -type Tuple = [T, ...T[]] | []; type TupleToInstances = { [I in keyof T]: Infer; } & any[]; -type Subclass any> = (new ( - ...args: any -) => InstanceType) & { - [K in keyof Class]: Class[K]; -} & { prototype: InstanceType }; - type PrivateInput = ProvableType | Subclass; type Method< diff --git a/src/lib/provable/types/struct.ts b/src/lib/provable/types/struct.ts index 4569997eeb..34d808647b 100644 --- a/src/lib/provable/types/struct.ts +++ b/src/lib/provable/types/struct.ts @@ -12,9 +12,9 @@ import type { IsPure, } from './provable-derivers.js'; import { Provable } from '../provable.js'; -import { DynamicProof, Proof } from '../../proof-system/zkprogram.js'; import { ProvablePure, ProvableType } from './provable-intf.js'; import { From, InferValue } from '../../../bindings/lib/provable-generic.js'; +import { DynamicProof, Proof } from '../../proof-system/proof.js'; // external API export { diff --git a/src/lib/util/types.ts b/src/lib/util/types.ts index 2bc40500d1..166eba12f3 100644 --- a/src/lib/util/types.ts +++ b/src/lib/util/types.ts @@ -1,6 +1,6 @@ import { assert } from './errors.js'; -export { AnyFunction, Tuple, TupleN, AnyTuple, TupleMap }; +export { AnyFunction, Tuple, TupleN, AnyTuple, TupleMap, Subclass }; type AnyFunction = (...args: any) => any; @@ -55,3 +55,11 @@ const TupleN = { type TupleRec = R['length'] extends N ? R : TupleRec; + +// classes + +type Subclass any> = (new ( + ...args: any +) => InstanceType) & { + [K in keyof Class]: Class[K]; +} & { prototype: InstanceType };