Skip to content

Commit 795b060

Browse files
authored
Merge pull request #1931 from o1-labs/feature/declare-proofs
Introduce recursive proofs from within ZkPrograms
2 parents fb2cf12 + bf1e275 commit 795b060

File tree

16 files changed

+659
-177
lines changed

16 files changed

+659
-177
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2222
### Added
2323

2424
- `ZkProgram` to support non-pure provable types as inputs and outputs https://github.com/o1-labs/o1js/pull/1828
25+
- API for recursively proving a ZkProgram method from within another https://github.com/o1-labs/o1js/pull/1931
26+
- `let recursive = Experimental.Recursive(program);`
27+
- `recursive.<methodName>(...args): Promise<PublicOutput>`
28+
- This also works within the same program, as long as the return value is type-annotated
2529
- Add `enforceTransactionLimits` parameter on Network https://github.com/o1-labs/o1js/issues/1910
2630
- Method for optional types to assert none https://github.com/o1-labs/o1js/pull/1922
2731
- Increased maximum supported amount of methods in a `SmartContract` or `ZkProgram` to 30. https://github.com/o1-labs/o1js/pull/1918
@@ -34,6 +38,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
3438

3539
## [2.1.0](https://github.com/o1-labs/o1js/compare/b04520d...e1bac02) - 2024-11-13
3640

41+
### Added
42+
3743
- Support secp256r1 in elliptic curve and ECDSA gadgets https://github.com/o1-labs/o1js/pull/1885
3844

3945
### Fixed

run-ci-tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ case $TEST_TYPE in
99
./run src/examples/zkapps/reducer/reducer-composite.ts --bundle
1010
./run src/examples/zkapps/composability.ts --bundle
1111
./run src/tests/fake-proof.ts
12+
./run src/tests/inductive-proofs-internal.ts --bundle
1213
./run tests/vk-regression/diverse-zk-program-run.ts --bundle
1314
;;
1415

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ import * as OffchainState_ from './lib/mina/actions/offchain-state.js';
148148
import * as BatchReducer_ from './lib/mina/actions/batch-reducer.js';
149149
import { Actionable } from './lib/mina/actions/offchain-state-serialization.js';
150150
import { InferProvable } from './lib/provable/types/struct.js';
151+
import { Recursive as Recursive_ } from './lib/proof-system/recursive.js';
151152
export { Experimental };
152153

153154
const Experimental_ = {
@@ -162,6 +163,8 @@ const Experimental_ = {
162163
namespace Experimental {
163164
export let memoizeWitness = Experimental_.memoizeWitness;
164165

166+
export let Recursive = Recursive_;
167+
165168
// indexed merkle map
166169
export let IndexedMerkleMap = Experimental_.IndexedMerkleMap;
167170
export type IndexedMerkleMap = IndexedMerkleMapBase;

src/lib/mina/account-update.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,6 @@ type LazyProof = {
655655
kind: 'lazy-proof';
656656
methodName: string;
657657
args: any[];
658-
previousProofs: Pickles.Proof[];
659658
ZkappClass: typeof SmartContract;
660659
memoized: { fields: Field[]; aux: any[] }[];
661660
blindingValue: Field;
@@ -2116,14 +2115,7 @@ async function addProof(
21162115

21172116
async function createZkappProof(
21182117
prover: Pickles.Prover,
2119-
{
2120-
methodName,
2121-
args,
2122-
previousProofs,
2123-
ZkappClass,
2124-
memoized,
2125-
blindingValue,
2126-
}: LazyProof,
2118+
{ methodName, args, ZkappClass, memoized, blindingValue }: LazyProof,
21272119
{ transaction, accountUpdate, index }: ZkappProverData
21282120
): Promise<Proof<ZkappPublicInput, Empty>> {
21292121
let publicInput = accountUpdate.toPublicInput(transaction);
@@ -2141,7 +2133,7 @@ async function createZkappProof(
21412133
blindingValue,
21422134
});
21432135
try {
2144-
return await prover(publicInputFields, MlArray.to(previousProofs));
2136+
return await prover(publicInputFields);
21452137
} catch (err) {
21462138
console.error(`Error when proving ${ZkappClass.name}.${methodName}()`);
21472139
throw err;
@@ -2151,7 +2143,7 @@ async function createZkappProof(
21512143
}
21522144
);
21532145

2154-
let maxProofsVerified = ZkappClass._maxProofsVerified!;
2146+
let maxProofsVerified = await ZkappClass.getMaxProofsVerified();
21552147
const Proof = ZkappClass.Proof();
21562148
return new Proof({
21572149
publicInput,

src/lib/mina/zkapp.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ import {
4444
import {
4545
analyzeMethod,
4646
compileProgram,
47+
computeMaxProofsVerified,
4748
Empty,
48-
getPreviousProofsForProver,
4949
MethodInterface,
5050
sortMethodArguments,
5151
VerificationKey,
5252
} from '../proof-system/zkprogram.js';
53-
import { Proof } from '../proof-system/proof.js';
53+
import { Proof, ProofClass } from '../proof-system/proof.js';
5454
import { PublicKey } from '../provable/crypto/signature.js';
5555
import {
5656
InternalStateType,
@@ -154,11 +154,6 @@ function method<K extends string, T extends SmartContract>(
154154
// FIXME: overriding a method implies pushing a separate method entry here, yielding two entries with the same name
155155
// this should only be changed once we no longer share the _methods array with the parent class (otherwise a subclass declaration messes up the parent class)
156156
ZkappClass._methods.push(methodEntry);
157-
ZkappClass._maxProofsVerified ??= 0;
158-
ZkappClass._maxProofsVerified = Math.max(
159-
ZkappClass._maxProofsVerified,
160-
methodEntry.numberOfProofs
161-
) as 0 | 1 | 2;
162157
let func = descriptor.value as AsyncFunction;
163158
descriptor.value = wrapMethod(func, ZkappClass, internalMethodEntry);
164159
}
@@ -341,8 +336,6 @@ function wrapMethod(
341336
{
342337
methodName: methodIntf.methodName,
343338
args: clonedArgs,
344-
// proofs actually don't have to be cloned
345-
previousProofs: getPreviousProofsForProver(actualArgs),
346339
ZkappClass,
347340
memoized,
348341
blindingValue,
@@ -433,7 +426,6 @@ function wrapMethod(
433426
{
434427
methodName: methodIntf.methodName,
435428
args: constantArgs,
436-
previousProofs: getPreviousProofsForProver(constantArgs),
437429
ZkappClass,
438430
memoized,
439431
blindingValue: constantBlindingValue,
@@ -593,10 +585,10 @@ class SmartContract extends SmartContractBase {
593585
rows: number;
594586
digest: string;
595587
gates: Gate[];
588+
proofs: ProofClass[];
596589
}
597590
>; // keyed by method name
598591
static _provers?: Pickles.Prover[];
599-
static _maxProofsVerified?: 0 | 1 | 2;
600592
static _verificationKey?: { data: string; hash: Field };
601593

602594
/**
@@ -644,6 +636,7 @@ class SmartContract extends SmartContractBase {
644636
forceRecompile = false,
645637
} = {}) {
646638
let methodIntfs = this._methods ?? [];
639+
let methodKeys = methodIntfs.map(({ methodName }) => methodName);
647640
let methods = methodIntfs.map(({ methodName }) => {
648641
return async (
649642
publicInput: unknown,
@@ -657,13 +650,15 @@ class SmartContract extends SmartContractBase {
657650
});
658651
// run methods once to get information that we need already at compile time
659652
let methodsMeta = await this.analyzeMethods();
660-
let gates = methodIntfs.map((intf) => methodsMeta[intf.methodName].gates);
653+
let gates = methodKeys.map((k) => methodsMeta[k].gates);
654+
let proofs = methodKeys.map((k) => methodsMeta[k].proofs);
661655
let { verificationKey, provers, verify } = await compileProgram({
662656
publicInputType: ZkappPublicInput,
663657
publicOutputType: Empty,
664658
methodIntfs,
665659
methods,
666660
gates,
661+
proofs,
667662
proofSystemTag: this,
668663
cache,
669664
forceRecompile,
@@ -689,6 +684,17 @@ class SmartContract extends SmartContractBase {
689684
return hash.toBigInt().toString(16);
690685
}
691686

687+
/**
688+
* The maximum number of proofs that are verified by any of the zkApp methods.
689+
* This is an internal parameter needed by the proof system.
690+
*/
691+
static async getMaxProofsVerified() {
692+
let methodData = await this.analyzeMethods();
693+
return computeMaxProofsVerified(
694+
Object.values(methodData).map((d) => d.proofs.length)
695+
);
696+
}
697+
692698
/**
693699
* Deploys a {@link SmartContract}.
694700
*
@@ -1189,7 +1195,7 @@ super.init();
11891195
try {
11901196
for (let methodIntf of methodIntfs) {
11911197
let accountUpdate: AccountUpdate;
1192-
let { rows, digest, gates, summary } = await analyzeMethod(
1198+
let { rows, digest, gates, summary, proofs } = await analyzeMethod(
11931199
ZkappPublicInput,
11941200
methodIntf,
11951201
async (publicInput, publicKey, tokenId, ...args) => {
@@ -1207,6 +1213,7 @@ super.init();
12071213
rows,
12081214
digest,
12091215
gates,
1216+
proofs,
12101217
};
12111218
if (printSummary) console.log(methodIntf.methodName, summary());
12121219
}

src/lib/proof-system/proof-system.unit-test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ it('pickles rule creation', async () => {
5454
expect(methodIntf).toEqual({
5555
methodName: 'main',
5656
args: [EmptyProof, Bool],
57-
numberOfProofs: 1,
5857
});
5958

6059
// store compiled tag
@@ -67,7 +66,8 @@ it('pickles rule creation', async () => {
6766
main as AnyFunction,
6867
{ name: 'mock' },
6968
methodIntf,
70-
[]
69+
[],
70+
[EmptyProof]
7171
);
7272

7373
await equivalentAsync(
@@ -133,7 +133,6 @@ it('pickles rule creation: nested proof', async () => {
133133
expect(methodIntf).toEqual({
134134
methodName: 'main',
135135
args: [NestedProof2],
136-
numberOfProofs: 2,
137136
});
138137

139138
// store compiled tag
@@ -146,7 +145,8 @@ it('pickles rule creation: nested proof', async () => {
146145
main as AnyFunction,
147146
{ name: 'mock' },
148147
methodIntf,
149-
[]
148+
[],
149+
[EmptyProof, EmptyProof]
150150
);
151151

152152
let dummy = await EmptyProof.dummy(Field(0), undefined, 0);

src/lib/proof-system/proof.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@ import type { Provable } from '../provable/provable.js';
1616
import { assert } from '../util/assert.js';
1717
import { Unconstrained } from '../provable/types/unconstrained.js';
1818
import { ProvableType } from '../provable/types/provable-intf.js';
19+
import { ZkProgramContext } from './zkprogram-context.js';
1920

2021
// public API
21-
export { ProofBase, Proof, DynamicProof };
22+
export { ProofBase, Proof, DynamicProof, ProofClass };
2223

2324
// internal API
2425
export { dummyProof, extractProofs, extractProofTypes, type ProofValue };
2526

2627
type MaxProofs = 0 | 1 | 2;
2728

29+
type ProofClass = Subclass<typeof ProofBase>;
30+
2831
class ProofBase<Input = any, Output = any> {
2932
static publicInputType: FlexibleProvable<any> = undefined as any;
3033
static publicOutputType: FlexibleProvable<any> = undefined as any;
@@ -40,6 +43,27 @@ class ProofBase<Input = any, Output = any> {
4043
maxProofsVerified: 0 | 1 | 2;
4144
shouldVerify = Bool(false);
4245

46+
/**
47+
* To verify a recursive proof inside a ZkProgram method, it has to be "declared" as part of
48+
* the method. This is done by calling `declare()` on the proof.
49+
*
50+
* Note: `declare()` is a low-level method that most users will not have to call directly.
51+
* For proofs that are inputs to the ZkProgram, it is done automatically.
52+
*
53+
* You can think of declaring a proof as a similar step as witnessing a variable, which introduces
54+
* that variable to the circuit. Declaring a proof will tell Pickles to add the additional constraints
55+
* for recursive proof verification.
56+
*
57+
* Similar to `Provable.witness()`, `declare()` is a no-op when run outside ZkProgram compilation or proving.
58+
* It returns `false` in that case, and `true` if the proof was actually declared.
59+
*/
60+
declare() {
61+
if (!ZkProgramContext.has()) return false;
62+
const ProofClass = this.constructor as Subclass<typeof ProofBase>;
63+
ZkProgramContext.declareProof({ ProofClass, proofInstance: this });
64+
return true;
65+
}
66+
4367
toJSON(): JsonProof {
4468
let fields = this.publicFields();
4569
return {

0 commit comments

Comments
 (0)