-
Notifications
You must be signed in to change notification settings - Fork 133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce recursive proofs from within ZkPrograms #1931
Conversation
@Trivo25 this is ready for review. In a follow-up PR, I also want to add a conditional version: This, in turn, would enable us to simplify the offchain state API, by handling the recursive settling proof internally. @45930 do you think that should be done? |
src/lib/proof-system/zkprogram.ts
Outdated
@@ -471,6 +466,50 @@ function ZkProgram< | |||
return compileOutput.verify(statement, proof.proof); | |||
} | |||
|
|||
let regularRecursiveProvers = mapObject(regularProvers, (prover, key) => { | |||
return async function proveRecursively_( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the implementation of the new recursive proving API
let input = fromFieldVars(publicInputType, publicInput); | ||
result = (await func(input, ...finalArgs)) as any; | ||
} | ||
proofs = ZkProgramContext.getDeclaredProofs(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of collecting all proofs inside this function as before, we get them from the new global context ZkProgramContext
, which is also written to by Proof.declare()
return program as any; | ||
return program; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I worked a bit on types in this file to reduce the amount of as any
, in particular it's nice to have this final sanity check that the returned program matches the output type. for that, I had to put a boolean into program.proofsEnabled
which we overwrite later with Object.defineProperty()
| { type: 'running' } | ||
| { type: 'exiting'; exitPromise: Promise<void> }; | ||
|
||
function WithThreadPool({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the added logic here comes from the bindings, see o1-labs/o1js-bindings#318
I forgot to declare the internally declared proofs to pickles in the top level of To fix this, I'll need to refactor a bit to find out the proofs prior to calling Because this caught me by surprise and seems like a dangerous footgun, I'm going to add runtime check that the number of dynamically returned proofs to be verified matches the number that was determined at compile time. |
src/lib/proof-system/zkprogram.ts
Outdated
setProofsEnabled(proofsEnabled: boolean) { | ||
doProving = proofsEnabled; | ||
}, | ||
|
||
proveRecursively, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will let the others be the judge if we want to express the API this way or if this is too confusing. I wonder if we can/should type this method in a certain way so its only available when its called in the correct context (within another program), and not in the plain native verison to avoid confusion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one note: proveRecursively
doesn't return you a proof (just the proof output), and it's therefore hard to confuse with the plain proving API.
like, if you need a proof, you'll figure out quickly that this API doesn't give you what you want.
(if you don't care about a proof, then it's fine to use this API outside a circuit, because it does run the method.. will just take very long due to the internal proving)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But for the example, I wouldn't understand how to use this. .recursiveProvers
seems slightly better, at least because it sounds like it may return an object. It still sounds like .recursiveProvers.baseCase()
will return a proof, so potentially .recursiveProverFunctions
is the most natural sounding.
I think there's an asterisk to this which is that zkprogram is still a very opaque API that people don't have an intuitive sense for, so I think we can only be so intuitive with the naming anyways.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah so it's not self-documenting I agree, but as you say, few things about zkprogram are.
which IMO just means it needs documentation! that documentation will be simpler than the existing docs, because the API is simpler to use.
compare, for example, what you currently have to do to define the Proof
class that belongs to a recursively verified program, i.e. ZkProgram.Proof()
. It's documented nowhere (in fact, our current docs show an API that doesn't even exist). Will be easier in the new API because you don't even have to define that proof class!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how I feel about the proposed API change. It is quite clean, but also hides a lot of detail, and perhaps makes it easier to write programs with accidental or intentional exploits. It seems to move the opposite direction of our new API design, which is more explicit and verbose. Since it's optional for developers, I don't feel strongly that we shouldn't release it.
I also don't love new APIs that are out ahead of the documentation. That's the state of where we are now, so I wouldn't block this release on that. But I intend to get to the point where I would block such a release for that reason in the future.
@45930 @Trivo25 If you're uncomfortable with introducing a different API out of the blue -- it would probably also work as a wrapper around ZkProgram. Something like this would produce less of an irreversible change: let program = ZkProgram(...);
let programRecursive = Experimental.Recursive(program);
// inside other program method
let output = await programRecursive.run(...inputs); Personally I think the API is awesome as is and would be easier to use and find on ZkProgram directly, but I don't want it to block the main addition of this PR, internal proof declaring, which is much more important. |
Note: I pushed one more commit f591f02 which emerged as a necessary fix in zksecurity/mina-credentials#86 when running internal proofs with Also, by experience on zksecurity/mina-credentials#86, I can now say that proof declaring works well in a complicated, real-world circuit. |
I would feel more comfortable with this experimental wrapper. Given that you've modified this PR a couple times already, it seems likely that more changes will eventually be needed. That way we can also give you the functionality you want, but have more time internally to get comfortable with these changes and think about how they fit into the v2 API changes. @Trivo25 would you also be ok with merging these changes as an experimental function? |
I think we should go for the wrapper and potentially move the functionality into zkProgram natively some time in the future |
cool, I'll push a refactor sometime soon |
All I Want For Christmas Is getting my PR merged 🎄 🎅🏻 🎄 🎁 🎄 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR enables to "declare" a recursively verified proof from within a ZkProgram method. Thus, recursive proofs no longer need to be inputs to the method, and therefore can be abstracted away much more easily.
bindings: o1-labs/o1js-bindings#318
The new low-level way of introducing a proof that hasn't been passed as method argument is as follows:
New Recursion API
see UPDATE below for final version of this API
On top of this, I added an even more pleasant API, which you can use in the typical case where the ZkProgram that produces the inner proof is in scope. For this, assume the proof in question is supposed to come from a method
innerProgram.run()
. Then, the entire logic above is now encapsulated inside the new propertyinnerProgram.proveRecursively.run()
:note: this expects the inputs (public and private) as arguments and returns the public output -- which is the natural way to think of calling a method.
Internally, we make sure that the inner proof's public input matches what the outer circuit passed in.
Caveats: of course we can't make any statement about the private inputs, because the inner proof inherently doesn't reveal those. so the private inputs must be understood only as hints to the recursive proving. Similarly, the inner proof doesn't guarantee the particular method that was called on the inner program: as usual with recursive proofs, all of the methods can produce it.
An e2e example, that I added to CI and also manually ran in the browser, is at
src/tests/inductive-proofs-internal.ts
UPDATE: after discussions, the API was changed to live separately from ZkProgram: