diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cde271da4..d1a19f971d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Added `VerificationKey.dummy()` method to get the dummy value of a verification key https://github.com/o1-labs/o1js/pull/1852 [@rpanic](https://github.com/rpanic) +### Changed + +- Make `Proof` a normal provable type, that can be witnessed and composed into Structs https://github.com/o1-labs/o1js/pull/1847, https://github.com/o1-labs/o1js/pull/1851 + - ZkProgram and SmartContract now also support private inputs that are not proofs themselves, but contain proofs nested within a Struct or array + - Only `SelfProof` can still not be nested because it needs special treatment + ## [1.8.0](https://github.com/o1-labs/o1js/compare/5006e4f...450943) - 2024-09-18 ### Added diff --git a/src/lib/mina/zkapp.ts b/src/lib/mina/zkapp.ts index aaa444b09d..6661d37c68 100644 --- a/src/lib/mina/zkapp.ts +++ b/src/lib/mina/zkapp.ts @@ -46,8 +46,6 @@ import { compileProgram, Empty, getPreviousProofsForProver, - methodArgumentsToConstant, - methodArgumentTypesAndValues, MethodInterface, sortMethodArguments, VerificationKey, @@ -157,7 +155,7 @@ function method( ZkappClass._maxProofsVerified ??= 0; ZkappClass._maxProofsVerified = Math.max( ZkappClass._maxProofsVerified, - methodEntry.proofs.length + methodEntry.numberOfProofs ) as 0 | 1 | 2; let func = descriptor.value as AsyncFunction; descriptor.value = wrapMethod(func, ZkappClass, internalMethodEntry); @@ -314,7 +312,7 @@ function wrapMethod( method.apply( this, actualArgs.map((a, i) => { - return Provable.witness(methodIntf.args[i].type, () => a); + return Provable.witness(methodIntf.args[i], () => a); }) ), noPromiseError @@ -342,10 +340,7 @@ function wrapMethod( methodName: methodIntf.methodName, args: clonedArgs, // proofs actually don't have to be cloned - previousProofs: getPreviousProofsForProver( - actualArgs, - methodIntf - ), + previousProofs: getPreviousProofsForProver(actualArgs), ZkappClass, memoized, blindingValue, @@ -387,7 +382,9 @@ function wrapMethod( let blindingValue = getBlindingValue(); let runCalledContract = async () => { - let constantArgs = methodArgumentsToConstant(methodIntf, actualArgs); + let constantArgs = methodIntf.args.map((type, i) => + Provable.toConstant(type, actualArgs[i]) + ); let constantBlindingValue = blindingValue.toConstant(); let accountUpdate = this.self; accountUpdate.body.callDepth = parentAccountUpdate.body.callDepth + 1; @@ -434,10 +431,7 @@ function wrapMethod( { methodName: methodIntf.methodName, args: constantArgs, - previousProofs: getPreviousProofsForProver( - constantArgs, - methodIntf - ), + previousProofs: getPreviousProofsForProver(constantArgs), ZkappClass, memoized, blindingValue: constantBlindingValue, @@ -527,7 +521,9 @@ function computeCallData( blindingValue: Field ) { let { returnType, methodName } = methodIntf; - let args = methodArgumentTypesAndValues(methodIntf, argumentValues); + let args = methodIntf.args.map((type, i) => { + return { type: ProvableType.get(type), value: argumentValues[i] }; + }); let input: HashInput = { fields: [], packed: [] }; for (let { type, value } of args) { diff --git a/src/lib/proof-system/proof-system.unit-test.ts b/src/lib/proof-system/proof-system.unit-test.ts index a868d9fa1e..03988d2cd1 100644 --- a/src/lib/proof-system/proof-system.unit-test.ts +++ b/src/lib/proof-system/proof-system.unit-test.ts @@ -4,6 +4,7 @@ import { UInt64 } from '../provable/int.js'; import { CompiledTag, Empty, + Void, ZkProgram, picklesRuleFromFunction, sortMethodArguments, @@ -44,11 +45,8 @@ it('pickles rule creation', async () => { expect(methodIntf).toEqual({ methodName: 'main', - args: [ - { type: EmptyProof, isProof: true }, - { type: Bool, isProof: false }, - ], - proofs: [EmptyProof], + args: [EmptyProof, Bool], + numberOfProofs: 1, }); // store compiled tag @@ -99,6 +97,76 @@ it('pickles rule creation', async () => { ); }); +class NestedProof extends Struct({ proof: EmptyProof, field: Field }) {} +const NestedProof2 = Provable.Array(NestedProof, 2); + +// type inference +NestedProof satisfies Provable<{ proof: Proof; 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], Proof); + + expect(methodIntf).toEqual({ + methodName: 'main', + args: [NestedProof2], + numberOfProofs: 2, + }); + + // store compiled tag + CompiledTag.store(EmptyProgram, 'mock tag'); + + // create pickles rule + let rule: Pickles.Rule = picklesRuleFromFunction( + Empty as ProvablePure, + Void as ProvablePure, + main as AnyFunction, + { name: 'mock' }, + methodIntf, + [] + ); + + 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], Proof); + }).toThrowError('mock.main() has more than two proof arguments'); +}); + // compile works with large inputs const N = 100_000; diff --git a/src/lib/proof-system/proof.ts b/src/lib/proof-system/proof.ts index 57cac9a41c..9111482cf9 100644 --- a/src/lib/proof-system/proof.ts +++ b/src/lib/proof-system/proof.ts @@ -10,12 +10,14 @@ import type { VerificationKey, JsonProof } from './zkprogram.js'; import { Subclass } from '../util/types.js'; import type { Provable } from '../provable/provable.js'; import { assert } from '../util/assert.js'; +import { Unconstrained } from '../provable/types/unconstrained.js'; +import { ProvableType } from '../provable/types/provable-intf.js'; // public API export { ProofBase, Proof, DynamicProof }; // internal API -export { dummyProof }; +export { dummyProof, extractProofs, extractProofTypes, type ProofValue }; type MaxProofs = 0 | 1 | 2; @@ -61,7 +63,7 @@ class ProofBase { this.maxProofsVerified = maxProofsVerified; } - static get provable() { + static get provable(): Provable { if ( this.publicInputType === undefined || this.publicOutputType === undefined @@ -71,7 +73,7 @@ class ProofBase { `class MyProof extends Proof { ... }` ); } - return provableProof( + return provableProof( this, this.publicInputType, this.publicOutputType, @@ -159,6 +161,10 @@ class Proof extends ProofBase { maxProofsVerified, }); } + + static get provable(): ProvableProof> { + return super.provable as any; + } } let sideloadedKeysCounter = 0; @@ -299,6 +305,10 @@ class DynamicProof extends ProofBase { proof: proof.proof, }) as InstanceType; } + + static get provable(): ProvableProof> { + return super.provable as any; + } } async function dummyProof(maxProofsVerified: 0 | 1 | 2, domainLog2: number) { @@ -308,10 +318,23 @@ async function dummyProof(maxProofsVerified: 0 | 1 | 2, domainLog2: number) { ); } +type ProofValue = { + publicInput: Input; + publicOutput: Output; + proof: Pickles.Proof; + maxProofsVerified: 0 | 1 | 2; +}; + +type ProvableProof< + Proof extends ProofBase, + InputV = any, + OutputV = any +> = Provable>; + function provableProof< Class extends Subclass>, - Input, - Output, + Input = any, + Output = any, InputV = any, OutputV = any >( @@ -319,15 +342,7 @@ function provableProof< input: Provable, output: Provable, defaultMaxProofsVerified?: MaxProofs -): Provable< - ProofBase, - { - publicInput: InputV; - publicOutput: OutputV; - proof: unknown; - maxProofsVerified: MaxProofs; - } -> { +): Provable, ProofValue> { return { sizeInFields() { return input.sizeInFields() + output.sizeInFields(); @@ -386,3 +401,30 @@ function provableProof< }, }; } + +function extractProofs(value: unknown): ProofBase[] { + if (value instanceof ProofBase) { + return [value]; + } + if (value instanceof Unconstrained) return []; + if (value instanceof Field) return []; + if (value instanceof Bool) return []; + + if (Array.isArray(value)) { + return value.flatMap((item) => extractProofs(item)); + } + + if (value === null) return []; + if (typeof value === 'object') { + return extractProofs(Object.values(value)); + } + + // functions, primitives + return []; +} + +function extractProofTypes(type: ProvableType) { + let value = ProvableType.synthesize(type); + let proofValues = extractProofs(value); + return proofValues.map((proof) => proof.constructor as typeof ProofBase); +} diff --git a/src/lib/proof-system/zkprogram.ts b/src/lib/proof-system/zkprogram.ts index 6c3165f99d..544cb53eb6 100644 --- a/src/lib/proof-system/zkprogram.ts +++ b/src/lib/proof-system/zkprogram.ts @@ -40,11 +40,21 @@ import { import { prefixToField } from '../../bindings/lib/binable.js'; import { prefixes } from '../../bindings/crypto/constants.js'; import { Subclass, Tuple } from '../util/types.js'; -import { dummyProof, DynamicProof, Proof, ProofBase } from './proof.js'; +import { + dummyProof, + DynamicProof, + extractProofs, + extractProofTypes, + Proof, + ProofBase, + ProofValue, +} from './proof.js'; import { featureFlagsFromGates, featureFlagsToMlOption, } from './feature-flags.js'; +import { emptyWitness } from '../provable/types/util.js'; +import { InferValue } from '../../bindings/lib/provable-generic.js'; // public API export { @@ -67,11 +77,6 @@ export { picklesRuleFromFunction, compileProgram, analyzeMethod, - emptyValue, - emptyWitness, - synthesizeMethodArguments, - methodArgumentsToConstant, - methodArgumentTypesAndValues, Prover, dummyBase64Proof, }; @@ -322,9 +327,7 @@ function ZkProgram< } if (!doProving) { - let previousProofs = MlArray.to( - getPreviousProofsForProver(args, methodIntfs[i]) - ); + let previousProofs = MlArray.to(getPreviousProofsForProver(args)); let publicOutput = await (methods[key].method as any)( publicInput, @@ -342,9 +345,7 @@ function ZkProgram< ); } let publicInputFields = toFieldConsts(publicInputType, publicInput); - let previousProofs = MlArray.to( - getPreviousProofsForProver(args, methodIntfs[i]) - ); + let previousProofs = MlArray.to(getPreviousProofsForProver(args)); let id = snarkContext.enter({ witnesses: args, inProver: true }); let result: UnwrapPromise>; @@ -448,8 +449,6 @@ type ZkProgram< } > = ReturnType>; -let i = 0; - class SelfProof extends Proof< PublicInput, PublicOutput @@ -478,25 +477,25 @@ function sortMethodArguments( selfProof: Subclass ): MethodInterface { // replace SelfProof with the actual selfProof + // TODO this does not handle SelfProof nested in inputs privateInputs = privateInputs.map((input) => input === SelfProof ? selfProof : input ); - // check if all arguments are provable, and record which are proofs - let args: { type: ProvableType; isProof: boolean }[] = - privateInputs.map((input) => { - if (!isProvable(input)) { - throw Error( - `Argument ${ - i + 1 - } of method ${methodName} is not a provable type: ${input}` - ); - } - return { type: input, isProof: isProof(input) }; - }); + // check if all arguments are provable + let args: ProvableType[] = privateInputs.map((input, i) => { + if (isProvable(input)) return input; - // store proofs separately as well - let proofs: Subclass[] = privateInputs.filter(isProof); + throw Error( + `Argument ${ + i + 1 + } of method ${methodName} is not a provable type: ${input}` + ); + }); + + // extract proofs to count them and for sanity checks + let proofs = args.flatMap(extractProofTypes); + let numberOfProofs = proofs.length; // don't allow base classes for proofs proofs.forEach((proof) => { @@ -509,13 +508,13 @@ function sortMethodArguments( }); // don't allow more than 2 proofs - if (proofs.length > 2) { + if (numberOfProofs > 2) { throw Error( `${programName}.${methodName}() has more than two proof arguments, which is not supported.\n` + `Suggestion: You can merge more than two proofs by merging two at a time in a binary tree.` ); } - return { methodName, args, proofs }; + return { methodName, args, numberOfProofs }; } function isProvable(type: unknown): type is ProvableType { @@ -529,7 +528,7 @@ function isProvable(type: unknown): type is ProvableType { ); } -function isProof(type: unknown): type is typeof ProofBase { +function isProofType(type: unknown): type is typeof ProofBase { // the third case covers subclasses return ( type === Proof || @@ -544,21 +543,14 @@ function isDynamicProof( return typeof type === 'function' && type.prototype instanceof DynamicProof; } -function getPreviousProofsForProver( - methodArgs: any[], - { args }: MethodInterface -) { - let proofs: ProofBase[] = []; - for (let i = 0; i < args.length; i++) { - if (args[i].isProof) proofs.push(methodArgs[i].proof); - } - return proofs; +function getPreviousProofsForProver(methodArgs: any[]) { + return methodArgs.flatMap(extractProofs).map((proof) => proof.proof); } type MethodInterface = { methodName: string; - args: { type: ProvableType; isProof: boolean }[]; - proofs: Subclass[]; + args: ProvableType[]; + numberOfProofs: number; returnType?: Provable; }; @@ -692,7 +684,7 @@ function analyzeMethod( method: (...args: any) => unknown ) { return Provable.constraintSystem(() => { - let args = synthesizeMethodArguments(methodIntf, true); + let args = methodIntf.args.map(emptyWitness); let publicInput = emptyWitness(publicInputType); // note: returning the method result here makes this handle async methods if (publicInputType === Undefined || publicInputType === Void) @@ -719,7 +711,7 @@ function picklesRuleFromFunction( publicOutputType: ProvablePure, func: (...args: unknown[]) => unknown, proofSystemTag: { name: string }, - { methodName, args, proofs: proofArgs }: MethodInterface, + { methodName, args }: MethodInterface, gates: Gate[] ): Pickles.Rule { async function main( @@ -729,21 +721,21 @@ function picklesRuleFromFunction( assert(!(inProver && argsWithoutPublicInput === undefined)); let finalArgs = []; let proofs: { - proofInstance: ProofBase; - classReference: Subclass>; + Proof: Subclass>; + proof: ProofBase; }[] = []; let previousStatements: Pickles.Statement[] = []; for (let i = 0; i < args.length; i++) { - let { type, isProof } = args[i]; + let type = args[i]; try { let value = Provable.witness(type, () => { - return argsWithoutPublicInput?.[i] ?? emptyValue(type); + return argsWithoutPublicInput?.[i] ?? ProvableType.synthesize(type); }); finalArgs[i] = value; - if (isProof) { - let Proof = type as Subclass>; - let proof = value as ProofBase; - proofs.push({ proofInstance: proof, classReference: Proof }); + + for (let proof of extractProofs(value)) { + let Proof = proof.constructor as Subclass>; + proofs.push({ Proof, proof }); let fields = proof.publicFields(); let input = MlFieldArray.to(fields.input); let output = MlFieldArray.to(fields.output); @@ -762,13 +754,13 @@ function picklesRuleFromFunction( result = await func(input, ...finalArgs); } - proofs.forEach(({ proofInstance, classReference }) => { - if (!(proofInstance instanceof DynamicProof)) return; + proofs.forEach(({ Proof, proof }) => { + if (!(proof instanceof DynamicProof)) return; // Initialize side-loaded verification key - const tag = classReference.tag(); + const tag = Proof.tag(); const computedTag = SideloadedTag.get(tag.name); - const vk = proofInstance.usedVerificationKey; + const vk = proof.usedVerificationKey; if (vk === undefined) { throw new Error( @@ -797,18 +789,19 @@ function picklesRuleFromFunction( publicOutput: MlFieldArray.to(publicOutput), previousStatements: MlArray.to(previousStatements), shouldVerify: MlArray.to( - proofs.map((proof) => proof.proofInstance.shouldVerify.toField().value) + proofs.map((proof) => proof.proof.shouldVerify.toField().value) ), }; } - if (proofArgs.length > 2) { + let proofs: Subclass[] = args.flatMap(extractProofTypes); + if (proofs.length > 2) { throw Error( `${proofSystemTag.name}.${methodName}() has more than two proof arguments, which is not supported.\n` + `Suggestion: You can merge more than two proofs by merging two at a time in a binary tree.` ); } - let proofsToVerify = proofArgs.map((Proof) => { + let proofsToVerify = proofs.map((Proof) => { let tag = Proof.tag(); if (tag === proofSystemTag) return { isSelf: true as const }; else if (isDynamicProof(Proof)) { @@ -849,38 +842,9 @@ function picklesRuleFromFunction( }; } -function synthesizeMethodArguments(intf: MethodInterface, asVariables = false) { - let empty = asVariables ? emptyWitness : emptyValue; - return intf.args.map(({ type }) => empty(type)); -} - -function methodArgumentsToConstant(intf: MethodInterface, args: any[]) { - return intf.args.map(({ type }, i) => Provable.toConstant(type, args[i])); -} - -type TypeAndValue = { type: Provable; value: T }; - -function methodArgumentTypesAndValues(intf: MethodInterface, args: unknown[]) { - return intf.args.map(({ type }, i): TypeAndValue => { - return { type: ProvableType.get(type), value: args[i] }; - }); -} - -function emptyValue(type: ProvableType) { - let provable = ProvableType.get(type); - return provable.fromFields( - Array(provable.sizeInFields()).fill(Field(0)), - provable.toAuxiliary() - ); -} - -function emptyWitness(type: ProvableType) { - return Provable.witness(type, () => emptyValue(type)); -} - function getMaxProofsVerified(methodIntfs: MethodInterface[]) { return methodIntfs.reduce( - (acc, { proofs }) => Math.max(acc, proofs.length), + (acc, { numberOfProofs }) => Math.max(acc, numberOfProofs), 0 ) as any as 0 | 1 | 2; } @@ -903,10 +867,16 @@ ZkProgram.Proof = function < name: string; publicInputType: PublicInputType; publicOutputType: PublicOutputType; -}) { - type PublicInput = InferProvable; - type PublicOutput = InferProvable; - return class ZkProgramProof extends Proof { +}): typeof Proof< + InferProvable, + InferProvable +> & { + provable: Provable< + Proof, InferProvable>, + ProofValue, InferValue> + >; +} { + return class ZkProgramProof extends Proof { static publicInputType = program.publicInputType; static publicOutputType = program.publicOutputType; static tag = () => program; diff --git a/src/lib/provable/option.ts b/src/lib/provable/option.ts index 86a71fb524..75d34b4a6b 100644 --- a/src/lib/provable/option.ts +++ b/src/lib/provable/option.ts @@ -1,5 +1,4 @@ import { InferValue } from '../../bindings/lib/provable-generic.js'; -import { emptyValue } from '../proof-system/zkprogram.js'; import { Provable } from './provable.js'; import { InferProvable, Struct } from './types/struct.js'; import { provable, ProvableInferPureFrom } from './types/provable-derivers.js'; @@ -77,7 +76,10 @@ function Option( fromValue(value: OptionOrValue) { if (value === undefined) - return { isSome: Bool(false), value: emptyValue(strictType) }; + return { + isSome: Bool(false), + value: ProvableType.synthesize(strictType), + }; // TODO: this isn't 100% robust. We would need recursive type validation on any nested objects to make it work if (typeof value === 'object' && 'isSome' in value) return PlainOption.fromValue(value as any); // type-cast here is ok, matches implementation @@ -103,7 +105,10 @@ function Option( static from(value?: V | T) { return value === undefined - ? new Option_({ isSome: Bool(false), value: emptyValue(strictType) }) + ? new Option_({ + isSome: Bool(false), + value: ProvableType.synthesize(strictType), + }) : new Option_({ isSome: Bool(true), value: strictType.fromValue(value), diff --git a/src/lib/provable/types/provable-intf.ts b/src/lib/provable/types/provable-intf.ts index 0a1b582017..a3e4bc4183 100644 --- a/src/lib/provable/types/provable-intf.ts +++ b/src/lib/provable/types/provable-intf.ts @@ -1,3 +1,4 @@ +import { createField } from '../core/field-constructor.js'; import type { Field } from '../field.js'; export { @@ -129,4 +130,14 @@ const ProvableType = { : type ) as ToProvable; }, + /** + * Create some value of type `T` from its provable type description. + */ + synthesize(type: ProvableType): T { + let provable = ProvableType.get(type); + return provable.fromFields( + Array(provable.sizeInFields()).fill(createField(0)), + provable.toAuxiliary() + ); + }, }; diff --git a/src/lib/provable/types/struct.ts b/src/lib/provable/types/struct.ts index 34d808647b..44249d937a 100644 --- a/src/lib/provable/types/struct.ts +++ b/src/lib/provable/types/struct.ts @@ -159,7 +159,7 @@ function Struct< } { class Struct_ { static type = provable(type); - static _isStruct: true; + static _isStruct: true = true; constructor(value: T) { Object.assign(this, value); diff --git a/src/lib/provable/types/util.ts b/src/lib/provable/types/util.ts new file mode 100644 index 0000000000..432b896740 --- /dev/null +++ b/src/lib/provable/types/util.ts @@ -0,0 +1,8 @@ +import { ProvableType } from './provable-intf.js'; +import { witness } from './witness.js'; + +export { emptyWitness }; + +function emptyWitness(type: ProvableType) { + return witness(type, () => ProvableType.synthesize(type)); +} diff --git a/src/tests/fake-proof.ts b/src/tests/fake-proof.ts index e1c3ebc232..ffe23a6d71 100644 --- a/src/tests/fake-proof.ts +++ b/src/tests/fake-proof.ts @@ -6,6 +6,9 @@ import { method, ZkProgram, verify, + Struct, + Field, + Proof, } from 'o1js'; import assert from 'assert'; @@ -30,9 +33,10 @@ const FakeProgram = ZkProgram({ }); class RealProof extends ZkProgram.Proof(RealProgram) {} +const Nested = Struct({ inner: RealProof }); const RecursiveProgram = ZkProgram({ - name: 'broken', + name: 'recursive', methods: { verifyReal: { privateInputs: [RealProof], @@ -40,6 +44,13 @@ const RecursiveProgram = ZkProgram({ proof.verify(); }, }, + verifyNested: { + privateInputs: [Field, Nested], + async method(_unrelated, { inner }) { + inner satisfies Proof; + inner.verify(); + }, + }, }, }); @@ -79,9 +90,9 @@ for (let proof of [fakeProof, dummyProof]) { const realProof = await RealProgram.make(UInt64.from(34)); // zkprogram accepts proof -const brokenProof = await RecursiveProgram.verifyReal(realProof); +const recursiveProof = await RecursiveProgram.verifyReal(realProof); assert( - await verify(brokenProof, programVk.data), + await verify(recursiveProof, programVk), 'recursive program accepts real proof' ); @@ -89,8 +100,27 @@ assert( let tx = await Mina.transaction(() => zkApp.verifyReal(realProof)); let [contractProof] = (await tx.prove()).proofs; assert( - await verify(contractProof!, contractVk.data), + await verify(contractProof!, contractVk), 'recursive contract accepts real proof' ); console.log('fake proof test passed 🎉'); + +// same test for nested proofs + +for (let proof of [fakeProof, dummyProof]) { + // zkprogram rejects proof (nested) + await assert.rejects(async () => { + await RecursiveProgram.verifyNested(Field(0), { inner: proof }); + }, 'recursive program rejects fake proof (nested)'); +} + +const recursiveProofNested = await RecursiveProgram.verifyNested(Field(0), { + inner: realProof, +}); +assert( + await verify(recursiveProofNested, programVk), + 'recursive program accepts real proof (nested)' +); + +console.log('fake proof test passed for nested proofs 🎉');