-
Notifications
You must be signed in to change notification settings - Fork 412
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
Refac/groth16 mpc #1372
base: master
Are you sure you want to change the base?
Refac/groth16 mpc #1372
Conversation
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.
lgtm, minor changes maybe
internal/generator/backend/template/zkpschemes/groth16/mpcsetup/utils.go.tmpl
Outdated
Show resolved
Hide resolved
r[i].SetRandom() | ||
func areInSubGroupG1(s []curve.G1Affine) bool { | ||
for i := range s { | ||
if !s[i].IsInSubGroup() { |
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.
that's not cheap, we don't have a batch subgroup check yet (@yelhousni wanted to add one) but shouldn't we parallelize since we assume the nominal case is everything is in subgroup ?
package utils | ||
|
||
// AppendRefs returns append(s, &v[0], &v[1], ...). | ||
func AppendRefs[T any](s []any, v []T) []any { |
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 this still needed with golang slices generic stuff ?
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.
Just went through the package docs. Nothing there does it compactly.
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.
Thanks Arya! It looks really nice. Imo I understood it mostly. There are a few comments though, see inline.
A few of the comments are nice to have and you don't need to address now. We could do it in separate PR:
- optional randomness source for contribution
- being able to change hash function for challenge computation for cool tricks a la providing SNARK of ceremony correctness
/*if a[0].IsOne() { | ||
A = A[1:] | ||
a = a[1:] | ||
}*/ |
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.
In all usages in the package we have len(A) == len(a)
I would add the check explicitly as a defensive measure. First to avoid out-of-bounds reads due to some calling error and secondly to ensure that all A
are scaled. Compared to scalar muls below it is negligible cost, but adds clarity.
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.
My concern with this is that its uses will become verbose and redundant. We'll get things like
scaleG1InPlace(c.G1.Tau[1:], tauUpdates[1:len(c.G1.Tau])
scaleG1InPlace(c.G1.AlphaTau, alphaUpdates[:len(c.G1.AlphaTau)])
scaleG1InPlace(c.G1.BetaTau, betaUpdates[:len(c.G1.BetaTau)])
where we previously had
scaleG1InPlace(c.G1.Tau[1:], tauUpdates[1:])
scaleG1InPlace(c.G1.AlphaTau, alphaUpdates)
scaleG1InPlace(c.G1.BetaTau, betaUpdates)
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.
Indeed - but currently the functions scaleG1InPlace
and scaleG2InPlace
are used only in one method in phase1.go
where we know anyway that len(tauUpdates) == len(c.G1.Tau)
(etc for AlphaTau and BetaTau).
Even more, now having a second look - I think we could just inline powers
and scaleG1InPlace
without defining utility methods at all and avoid too early generefication. Then we can avoid allocating the slices tauUpdates
, betaUpdates
and alphaUpdates
.
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.
And in order to make it parallel, we can use utils.Parallelize
and then within compute s = f^start
as the starting point and then at every loop multiply s by f and then do a scalar multiplication.
/*if a[0].IsOne() { | ||
A = A[1:] | ||
a = a[1:] | ||
}*/ |
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.
Similarly would add check len(A) == len(a)
explicitly. And imo it is OK to panic here, no need to handle errors.
) | ||
|
||
func ExtractKeys(srs1 *Phase1, srs2 *Phase2, evals *Phase2Evaluations, nConstraints int) (pk groth16.ProvingKey, vk groth16.VerifyingKey) { | ||
// Seal performs the final contribution and outputs the proving and verifying keys. |
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.
Add documentation what the beaconChallenge
should be. Is it an identifier for the current ceremony? What happens if I run Seal independently on same Phase2
with different beaconChallenge
- is there any dependence between the output Groth16 SRS?
|
||
func testAll(t *testing.T, nbContributionsPhase1, nbContributionsPhase2 int) { | ||
assert := require.New(t) |
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.
There is probably some import cycle involved, but couldn't you just do assert := test.NewAssert(t)
? Then the returned test.Assert
object has helper methods for proving etc.
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.
Which methods do you have in mind? Since the setup has to be in a custom way (imitating how an actual multiparty ceremony would go with objects being written to and read from the network) there is only the proof and verification stages to get help with, and those are short enough imo.
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.
There is method below proveVerifyCircuit
, but indeed it uses fixed proving and verifying key and test.Assert
doesn't currently have a way to test it like this. But still, test.Assert
is a very simple wrapper around require.Assertions
, keeping all the methods what require.Assertions
has. And imo this unifies the testing style if we use same instance everywhere.
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.
Ah so we generally better not use the testify repo directly?
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.
At least I would use the wrapper we have - then the test implementations across different gnark packages are more coherent imo.
@@ -0,0 +1,286 @@ | |||
package mpcsetup |
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.
File only generated for BN254? I would code-generate just in case.
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 idea was not to clog the CI with unit tests. The full test is still generated for all curves. Ignoring the unit tests saves 5 seconds per curve on my laptop. Perhaps not worth it?
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.
In that case, I would still code generate, but maybe conditionally add test.Skip()
in case the curve isn't BN254. Then atleast we ensure that everything is syntactically correct and the unit test is covered in code generation.
} | ||
|
||
func (phase1 *Phase1) hash() []byte { | ||
func (p *Phase1) hash() []byte { |
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.
And imo, the method could also be public and defined:
func (p *Phase) Challenge(previous []byte) []byte
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.
Would it perhaps be better to remove the challenge
field entirely, and just feed it as input to the Contribute
and Verify
functions?
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 downside of this approach is that when sending contributions on the wire the user has to read/write two objects rather than one.
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 in practice do we need to send challenge on the wire? When the user returns contribution to the sequencer, then it doesn't need to send the challenge, sequencer can compute it on its own (and it actually already does it, assuming that it verifies the contributions).
Now, the question imo is if the user is sent the tip of the ceremony for starting their contribution, then should it verify the full state of the current ceremony. Imo for me it would make sense it should, to ensure that it doesn't contribute to some fork etc. From practical point of view, it is probably too heavy for the users to verify and it could only contribute based on current trusted tip (i.e. ceremony tip and running challenge). And imo then it makes sense that challenge is separate, to indicate its optionality. And in this case imo we can provide it separately to Contribute
and Verify
But its only my gut feeling, maybe it is okay to keep it in, but add comment that it is "trusted" value and should be verfied in full.
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 in practice do we need to send challenge on the wire?
Yes, any new contributor need to receive the challenge used by the last contribution in order to compute its own challenge (so that the channel reflects the entire protocol transcript)
You are right that the field can be omitted when a contribution is being sent back to the sequencer.
I don't think we should require contributors to verify the whole protocol transcript. That goes against two of the protocol's major design goals:
- Low entry barrier for contributions: I should be able to contribute relatively easily on a small-ish machine in a short amount of time
- Indefinite run: The protocol is never truly over. We should always be able to enhance our SRS with more contributions, so the number of contributions is not a constant. So especially in light of (1) the verification effort gets prohibitive.
but add comment that it is "trusted" value
A trusted value from the contributor's perspective you mean? Because the verifier does reconstruct it, in the same way as you described in the first paragraph:
https://github.com/Consensys/gnark/blob/refac/groth16-mpc/backend/groth16/bn254/mpcsetup/phase1.go#L168
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.
Yep, I think from practical point of view it doesn't make sense for the contributor to verify the whole chain, only the previous contribution on top it builds its contribution. And I guess it is fine to keep Challenge
field inside the structs, makes easier to serialize and transport.
@@ -21,8 +22,8 @@ func RoundTripCheck(from any, to func() any) error { | |||
if err != nil { | |||
return err | |||
} | |||
if !reflect.DeepEqual(from, r) { | |||
return errors.New("reconstructed object don't match original (ReadFrom)") | |||
if err = equal(from, r); err != nil { |
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 would keep it as is, imo there may be unwanted side-effects of using Equal
if exists - there are already a few types in gnark which have the Equal method defined and they may be more strict than needed.
It seems like it is used to avoid comparing Challenge
field in a test. But at least for me it would be clearer to manually empty the Challenge field manually when needed in the test. This current approach may break imo other roundtrip tests.
} | ||
if !sameRatio(contribution.Parameters.G1.AlphaTau[0], current.Parameters.G1.AlphaTau[0], alphaR, contribution.PublicKeys.Alpha.XR) { | ||
return errors.New("couldn't verify that [α]₁ is based on previous contribution") | ||
next.Challenge = challenge |
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.
My assumption was that verification wouldn't change the inputs, but it does as we write next.Challenge
. This imo may have practical side effects that the contributions are not static (a la when hashing serialized proofs). Instead, I would provide the challenge as argument to Verify
and then VerifyPhase1
automatically carries the continuation over to next round (with computing it using newChallenge := Phase1.Hash(challenge)
)
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.
It's only changed if it is empty.
if len(next.Challenge) != 0 && !bytes.Equal(next.Challenge, challenge) { | ||
return errors.New("the challenge does not match the previous contribution's hash") | ||
} | ||
next.Challenge = challenge |
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.
Similarly as for Phase1 imo Phase2.Verify shouldn't change the inputs. See the corresponding comment.
return ccs.(*cs.R1CS) | ||
}) | ||
|
||
func getTestCircuit() *cs.R1CS { |
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.
And now, we don't even need to have getTestCircuit
function separately, we can just remove the current getTestCircuit
function, rename onceCircuit
variable to getTestCircuit
and it works exactly the same.
Description
#1297, but using tools from Consensys/gnark-crypto#589.
Fixes # (issue)
Type of change
How has this been tested?
How has this been benchmarked?
Checklist:
golangci-lint
does not output errors locally