-
Notifications
You must be signed in to change notification settings - Fork 170
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
Refactor to allow non-determinisim. #76
Conversation
34db470
to
5500c6f
Compare
src/lib.rs
Outdated
<G1 as Group>::Scalar::one(), | ||
<G2 as Group>::Scalar::zero(), | ||
); | ||
assert!(res.is_ok()); | ||
let recursive_snark = res.unwrap(); | ||
|
||
assert_eq!(num_steps, recursive_snark.num_steps); |
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.
Same comment as above.
Thanks, @porcuquine for the PR! I left some preliminary comments. Given the focus is to allow non-determinism, could we add/extend one of the tests to include some non-deterministic step function (as a sanity check and to demonstrate the use of public APIs)? |
Yes, that was my next plan. I'll put something together and push it soon. |
eb2d59e
to
6b097a9
Compare
Okay, I added a test exercising the new procedure by precomputing nondeterministic hints and storing them in the circuit. I needed to tweak the implementation a little to make this work (so the test was especially useful!). This also revealed that the concrete circuits never needed to stored in the RecursiveSNARK, so I removed. |
src/lib.rs
Outdated
let pp = | ||
PublicParams::<G1, G2, TrivialTestCircuit<S1>, CubicIncrementingCircuit<S2>, U1, U2>::setup( | ||
TrivialTestCircuit::default(), | ||
CubicIncrementingCircuit::new(IO::Empty(&p)), |
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 PoseidonConstants are used here, in the blank/empty circuit.
b18baa5
to
f4cdf76
Compare
) -> Result<Self, NovaError> { | ||
if num_steps == 0 { |
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.
How do we handle this case without this code in the new version?
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 error will be returned if an iterator has no first item: https://github.com/microsoft/Nova/pull/76/files/3986df4f2c580d49f449f84ab84c02eb0db1668b#diff-b1a35a68f14e696205874893c07fd24fdb88882b47c23cc0e0c80a30c7d53759R394-R401
} | ||
} | ||
|
||
/// A helper trait for computing a step of the incremental computation (i.e., F itself) |
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.
Is there a (fundamental) reason to have two traits since it seems like we end up implementing both StepCompute and StepCircuit on the same types right?
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.
Yes, a caller might want more control, in which case they can implement Iterator directly. This will bypass the iterator automatically created using ComputeStep
. That is also why prove_with_iterators
is public. When the caller implements an iterator, they can bypass prove
and call the lower-level function.
params_primary: NIFSVerifierCircuitParams, | ||
params_secondary: NIFSVerifierCircuitParams, | ||
c_primary: C1, | ||
c_secondary: C2, | ||
a1: Option<PoseidonConstants<<G1 as Group>::Scalar, A1>>, |
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 think we should be using ROConstants
instead of PoseidonConstants
(there was a TODO at the top of the file to completely avoid including poseidon.rs from lib.rs). In the short term, if the goal is to support a few arities of interest (e.g., 2 for VDFs and 6 for Lurk), could we just add those aritites to the hard-coded constants that we generate? This will likely avoid a lot of complexity. What do you think?
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.
We could then make implementations of ROConstants
be able to handle arbitrary arities via the sponge construction.
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 agree with you on the general topology, and I have pushed a commit which I think finally addresses the issues. I moved the arity poseidon constants into the verifier circuit. I didn't put them in ROConstants
directly for several related reasons:
- If we retain generality, that would require putting an
Arity
type parameter everywhere, including places it didn't belong. - If we discard generality, it would actually have added a lot of complexity and cost around 'dynamic allocation' of specific constants.
- They're not really part of the RO anyway. As implemented here, they're a helper for the application function F.
In any case, my recent commit now successfully hides the PoseidonConstants from the caller almost completely. In the case that a caller wants to implement the ComputeStep
trait for a non-unary function, they need to handle (but not generate) constants in the compute_inner
method. This can probably also be eliminated, but I think it's livable for now.
@@ -138,26 +223,30 @@ where | |||
zn_secondary: G2::Scalar, | |||
_p_c1: PhantomData<C1>, | |||
_p_c2: PhantomData<C2>, | |||
a1: PhantomData<A1>, |
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.
Should these be _p_a1
and _p_a2
?
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.
These fields are actually used to simplify initializing IO::Vals
later.
@srinathsetty I have resolved the conflicts with |
I have pushed a few more small changes, including a small refactor allowing for cleaner trait implementations. The first-order goal of this PR is now complete; the interfaces are now sufficient to use Nova recursion in the VDF proof as implemented here: protocol/vdf#14. |
I'm closing this, since its purpose is and/or will be fulfilled by other work now. |
This PR refactors proving in a number of ways. At least some of these changes are necessary to support determinism in the computation of F. The existing code didn't allow this, since
compute
was always called on the same circuit prototype contained in the public parameters.This change also makes
num_steps
optional, and the state accumulated in aRecursiveSNARK
tracks the actual number of steps used. This allows for applications where the number of steps is not known in advance.It's likely that further small changes may be needed when integrating downstream consumers who will need this functionality, but until integration happens, it is difficult to anticipate what that may entail beyond my best guesses here.
NOTE: in the future, we should support a recursive proof which does not expose the number of steps. This will be important for applications in which knowing the number of steps required to compute a final result leaks information. Having the option to generate proofs without revealing the number of steps will eliminate effective 'timing attacks' on such applications.
UPDATE: I added support for non-unary functions, building on the first part. I know that makes this a fairly large PR, but I had to rework a fair amount of the foundation to get here.
The basic idea is that for higher-arity functions, the implementer should implement methods (
compute_inner
andsynthesize_step_inner
, which operate on and return a vector of scalars. The goal was to make the actual mechanism as hidden from the caller/user as possible, but some leakage was unavoidable so far.Under the hood, at each step, the inputs are hashed to get the true unary input for the step function, F — and the outputs are again hashed to produce the unary output. Both hashes are proved in the circuit.
Because we need to avoid storing the initial input (Z0) in the public parameters, it's necessary to first create an 'empty' input used to generate the R1CS shape. That means the caller does (unfortunately) have to create the poseidon constants for the relevant scalar and arity. This is not especially difficult, but it would be nicer if it could have been avoided. This wart means that the constants are generated twice in this case. With more machinery, the public parameters and IO enum could be more symbiotic, but for now I think this is a reasonable tradeoff.