-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: aggregate votes circuit (#5)
* Gnark circuit implementation to aggregate up to 10 vote verifier proofs (on BLS12-377) into a single one (BW6_761). * It also includes the implementation of a Gnark dummy circuit (`dummyCircuit`) that matches the original with an input and properties to be used to fill the remaining proofs to reach the maximum, if the proofs to aggregate are less than this maximum, including the helpers to work with. * Test refactoring, skipping heavy tests without `RUN_CIRCUIT_TESTS` envvar and some comments and minor changes.
- Loading branch information
1 parent
dd7d1b7
commit 41d1e4a
Showing
20 changed files
with
1,194 additions
and
481 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
// aggregator package contains the Gnark circuit defiinition that aggregates | ||
// some votes and proves the validity of the aggregation. The circuit checks | ||
// every single verification proof generating a single proof for the whole | ||
// aggregation. Every voter proof should use the same values for the following | ||
// inputs: | ||
// - MaxCount | ||
// - ForceUniqueness | ||
// - MaxValue | ||
// - MinValue | ||
// - MaxTotalCost | ||
// - MinTotalCost | ||
// - CostExp | ||
// - CostFromWeight | ||
// - EncryptionPubKey | ||
// - ProcessId | ||
// - CensusRoot | ||
// | ||
// All these values are common for the same process. | ||
// | ||
// The circuit also checks the other inputs that are unique for each voter: | ||
// - Nullifier | ||
// - Commitment | ||
// - Address | ||
// - EncryptedBallots | ||
// - VerifyProof (generated with the VerifyVoteCircuit) | ||
package aggregator | ||
|
||
import ( | ||
"github.com/consensys/gnark/frontend" | ||
"github.com/consensys/gnark/std/algebra/native/sw_bls12377" | ||
"github.com/consensys/gnark/std/hash/mimc" | ||
"github.com/consensys/gnark/std/math/bits" | ||
"github.com/consensys/gnark/std/recursion/groth16" | ||
"github.com/vocdoni/vocdoni-z-sandbox/circuits/ballotproof" | ||
) | ||
|
||
const ( | ||
MaxVotes = 10 | ||
MaxFields = ballotproof.NFields | ||
) | ||
|
||
type AggregatorCircuit struct { | ||
InputsHash frontend.Variable `gnark:",public"` | ||
ValidVotes frontend.Variable `gnark:",public"` | ||
// The following variables are priv-public inputs, so should be hashed and | ||
// compared with the InputsHash. All the variables should be hashed in the | ||
// same order as they are defined here. | ||
MaxCount frontend.Variable // Part of InputsHash | ||
ForceUniqueness frontend.Variable // Part of InputsHash | ||
MaxValue frontend.Variable // Part of InputsHash | ||
MinValue frontend.Variable // Part of InputsHash | ||
MaxTotalCost frontend.Variable // Part of InputsHash | ||
MinTotalCost frontend.Variable // Part of InputsHash | ||
CostExp frontend.Variable // Part of InputsHash | ||
CostFromWeight frontend.Variable // Part of InputsHash | ||
EncryptionPubKey [2]frontend.Variable // Part of InputsHash | ||
ProcessId frontend.Variable // Part of InputsHash | ||
CensusRoot frontend.Variable // Part of InputsHash | ||
Nullifiers [MaxVotes]frontend.Variable // Part of InputsHash | ||
Commitments [MaxVotes]frontend.Variable // Part of InputsHash | ||
Addresses [MaxVotes]frontend.Variable // Part of InputsHash | ||
EncryptedBallots [MaxVotes][MaxFields][2][2]frontend.Variable // Part of InputsHash | ||
// VerifyCircuit proofs | ||
VerifyProofs [MaxVotes]groth16.Proof[sw_bls12377.G1Affine, sw_bls12377.G2Affine] | ||
VerifyPublicInputs [MaxVotes]groth16.Witness[sw_bls12377.ScalarField] | ||
// VerificationKeys should contain the dummy circuit and the main circuit | ||
// verification keys in that particular order | ||
VerificationKeys [2]groth16.VerifyingKey[sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT] `gnark:"-"` | ||
} | ||
|
||
func (c *AggregatorCircuit) checkInputs(api frontend.API) error { | ||
// group all the inputs to hash them | ||
inputs := []frontend.Variable{ | ||
c.MaxCount, c.ForceUniqueness, c.MaxValue, c.MinValue, c.MaxTotalCost, | ||
c.MinTotalCost, c.CostExp, c.CostFromWeight, c.EncryptionPubKey[0], | ||
c.EncryptionPubKey[1], c.ProcessId, c.CensusRoot, | ||
} | ||
inputs = append(inputs, c.Nullifiers[:]...) | ||
inputs = append(inputs, c.Commitments[:]...) | ||
inputs = append(inputs, c.Addresses[:]...) | ||
// include flattened EncryptedBallots | ||
for _, voterBallots := range c.EncryptedBallots { | ||
for _, ballot := range voterBallots { | ||
inputs = append(inputs, ballot[0][0], ballot[0][1], ballot[1][0], ballot[1][1]) | ||
} | ||
} | ||
// hash the inputs | ||
hFn, err := mimc.NewMiMC(api) | ||
if err != nil { | ||
return err | ||
} | ||
hFn.Write(inputs...) | ||
// compare the hash with the provided InputsHash | ||
api.AssertIsEqual(c.InputsHash, hFn.Sum()) | ||
return nil | ||
} | ||
|
||
func (c *AggregatorCircuit) Define(api frontend.API) error { | ||
// check the inputs of the circuit | ||
if err := c.checkInputs(api); err != nil { | ||
return err | ||
} | ||
// initialize the verifier | ||
verifier, err := groth16.NewVerifier[sw_bls12377.ScalarField, sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT](api) | ||
if err != nil { | ||
return err | ||
} | ||
// verify each proof with the provided public inputs and the fixed | ||
// verification key | ||
validProofs := bits.ToBinary(api, c.ValidVotes) | ||
expectedValidVotes, totalValidVotes := frontend.Variable(0), frontend.Variable(0) | ||
for i := 0; i < len(c.VerifyProofs); i++ { | ||
vk, err := verifier.SwitchVerificationKey(validProofs[i], c.VerificationKeys[:]) | ||
if err != nil { | ||
return err | ||
} | ||
if err := verifier.AssertProof(vk, c.VerifyProofs[i], c.VerifyPublicInputs[i]); err != nil { | ||
return err | ||
} | ||
expectedValidVotes = api.Add(expectedValidVotes, validProofs[i]) | ||
totalValidVotes = api.Add(totalValidVotes, validProofs[i]) | ||
} | ||
api.AssertIsEqual(expectedValidVotes, totalValidVotes) | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package aggregator | ||
|
||
import ( | ||
"os" | ||
"testing" | ||
"time" | ||
|
||
"github.com/consensys/gnark-crypto/ecc" | ||
"github.com/consensys/gnark/backend" | ||
stdgroth16 "github.com/consensys/gnark/std/recursion/groth16" | ||
"github.com/consensys/gnark/test" | ||
qt "github.com/frankban/quicktest" | ||
"github.com/vocdoni/vocdoni-z-sandbox/util" | ||
) | ||
|
||
func TestAggregatorCircuit(t *testing.T) { | ||
if os.Getenv("RUN_CIRCUIT_TESTS") == "" { | ||
t.Skip("skipping circuit tests...") | ||
} | ||
c := qt.New(t) | ||
// inputs generation | ||
now := time.Now() | ||
processId := util.RandomBytes(20) | ||
_, placeholder, assigments, err := GenInputsForTest(processId, 3) | ||
c.Assert(err, qt.IsNil) | ||
c.Logf("inputs generation tooks %s", time.Since(now).String()) | ||
// proving | ||
now = time.Now() | ||
assert := test.NewAssert(t) | ||
assert.SolvingSucceeded(placeholder, assigments, | ||
test.WithCurves(ecc.BW6_761), test.WithBackends(backend.GROTH16), | ||
test.WithProverOpts(stdgroth16.GetNativeProverOptions(ecc.BN254.ScalarField(), ecc.BW6_761.ScalarField()))) | ||
c.Logf("proving tooks %s", time.Since(now).String()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package aggregator | ||
|
||
import ( | ||
"errors" | ||
|
||
"github.com/consensys/gnark/constraint" | ||
"github.com/consensys/gnark/frontend" | ||
) | ||
|
||
type dummyCircuit struct { | ||
nbConstraints int | ||
SecretInput frontend.Variable `gnark:",secret"` | ||
PublicInputs frontend.Variable `gnark:",public"` | ||
} | ||
|
||
func (c *dummyCircuit) Define(api frontend.API) error { | ||
cmtr, ok := api.(frontend.Committer) | ||
if !ok { | ||
return errors.New("api is not a commiter") | ||
} | ||
secret, err := cmtr.Commit(c.SecretInput) | ||
if err != nil { | ||
return err | ||
} | ||
api.AssertIsDifferent(secret, 0) | ||
|
||
res := api.Mul(c.SecretInput, c.SecretInput) | ||
for i := 2; i < c.nbConstraints; i++ { | ||
res = api.Mul(res, c.SecretInput) | ||
} | ||
api.AssertIsEqual(c.PublicInputs, res) | ||
return nil | ||
} | ||
|
||
// DummyPlaceholder function returns the placeholder of a dummy circtuit for | ||
// the constraint.ConstraintSystem provided. | ||
func DummyPlaceholder(mainCircuit constraint.ConstraintSystem) *dummyCircuit { | ||
return &dummyCircuit{nbConstraints: mainCircuit.GetNbConstraints()} | ||
} | ||
|
||
// DummyPlaceholder function returns the assigment of a dummy circtuit. | ||
func DummyAssigment() *dummyCircuit { | ||
return &dummyCircuit{PublicInputs: 1, SecretInput: 1} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
package aggregator | ||
|
||
import ( | ||
"fmt" | ||
"math/big" | ||
|
||
"github.com/consensys/gnark-crypto/ecc" | ||
"github.com/consensys/gnark/backend/groth16" | ||
"github.com/consensys/gnark/backend/witness" | ||
"github.com/consensys/gnark/constraint" | ||
"github.com/consensys/gnark/frontend" | ||
"github.com/consensys/gnark/frontend/cs/r1cs" | ||
"github.com/consensys/gnark/std/algebra/native/sw_bls12377" | ||
stdgroth16 "github.com/consensys/gnark/std/recursion/groth16" | ||
) | ||
|
||
// EncodeProofsSelector function returns a number that its base2 representation | ||
// contains the first nValidProofs bits set to one. It allows to encode the | ||
// number of valid proofs as selector to switch between main circuit vk and the | ||
// dummy one. | ||
func EncodeProofsSelector(nValidProofs int) *big.Int { | ||
// no valid number if nValidProofs <= 0 | ||
if nValidProofs <= 0 { | ||
return big.NewInt(0) | ||
} | ||
// (1 << nValidProofs) - 1 gives a binary number with nValidProofs ones | ||
// compute (1 << n) - 1 | ||
maxNum := big.NewInt(1) | ||
// left shift by 'n' | ||
maxNum.Lsh(maxNum, uint(nValidProofs)) | ||
// subtract 1 to get all n set to 1 | ||
return maxNum.Sub(maxNum, big.NewInt(1)) | ||
} | ||
|
||
// FillWithDummyFixed function fills the placeholder and the assigments | ||
// provided with a dummy circuit stuff and proofs compiled for the main | ||
// constraint.ConstraintSystem provided. It starts to fill from the index | ||
// provided and fixes the dummy verification key. Returns an error if | ||
// something fails. | ||
func FillWithDummyFixed(placeholder, assigments *AggregatorCircuit, main constraint.ConstraintSystem, fromIdx int) error { | ||
// compile the dummy circuit for the main | ||
dummyCCS, pubWitness, proof, vk, err := compileAndVerifyCircuit( | ||
DummyPlaceholder(main), DummyAssigment(), | ||
ecc.BW6_761.ScalarField(), ecc.BLS12_377.ScalarField()) | ||
if err != nil { | ||
return err | ||
} | ||
// set fixed dummy vk in the placeholders | ||
placeholder.VerificationKeys[0], err = stdgroth16.ValueOfVerifyingKeyFixed[sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT](vk) | ||
if err != nil { | ||
return fmt.Errorf("fix dummy vk error: %w", err) | ||
} | ||
// parse dummy proof and witness | ||
dummyProof, err := stdgroth16.ValueOfProof[sw_bls12377.G1Affine, sw_bls12377.G2Affine](proof) | ||
if err != nil { | ||
return fmt.Errorf("dummy proof value error: %w", err) | ||
} | ||
dummyWitness, err := stdgroth16.ValueOfWitness[sw_bls12377.ScalarField](pubWitness) | ||
if err != nil { | ||
return fmt.Errorf("dummy witness value error: %w", err) | ||
} | ||
// set some dummy values in others assigments variables | ||
dummyValue := frontend.Variable(0) | ||
var dummyEncryptedBallots [MaxFields][2][2]frontend.Variable | ||
for i := 0; i < MaxFields; i++ { | ||
dummyEncryptedBallots[i] = [2][2]frontend.Variable{ | ||
{dummyValue, dummyValue}, {dummyValue, dummyValue}, | ||
} | ||
} | ||
// fill placeholders and assigments dummy values | ||
for i := range assigments.VerifyProofs { | ||
if i >= fromIdx { | ||
placeholder.VerifyProofs[i] = stdgroth16.PlaceholderProof[sw_bls12377.G1Affine, sw_bls12377.G2Affine](dummyCCS) | ||
placeholder.VerifyPublicInputs[i] = stdgroth16.PlaceholderWitness[sw_bls12377.ScalarField](dummyCCS) | ||
|
||
assigments.Nullifiers[i] = dummyValue | ||
assigments.Commitments[i] = dummyValue | ||
assigments.Addresses[i] = dummyValue | ||
assigments.EncryptedBallots[i] = dummyEncryptedBallots | ||
assigments.VerifyProofs[i] = dummyProof | ||
assigments.VerifyPublicInputs[i] = dummyWitness | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func compileAndVerifyCircuit(placeholder, assigment frontend.Circuit, outer *big.Int, field *big.Int) (constraint.ConstraintSystem, witness.Witness, groth16.Proof, groth16.VerifyingKey, error) { | ||
ccs, err := frontend.Compile(field, r1cs.NewBuilder, placeholder) | ||
if err != nil { | ||
return nil, nil, nil, nil, fmt.Errorf("compile error: %w", err) | ||
} | ||
pk, vk, err := groth16.Setup(ccs) | ||
if err != nil { | ||
return nil, nil, nil, nil, fmt.Errorf("setup error: %w", err) | ||
} | ||
fullWitness, err := frontend.NewWitness(assigment, field) | ||
if err != nil { | ||
return nil, nil, nil, nil, fmt.Errorf("full witness error: %w", err) | ||
} | ||
proof, err := groth16.Prove(ccs, pk, fullWitness, stdgroth16.GetNativeProverOptions(outer, field)) | ||
if err != nil { | ||
return nil, nil, nil, nil, fmt.Errorf("proof error: %w", err) | ||
} | ||
publicWitness, err := fullWitness.Public() | ||
if err != nil { | ||
return nil, nil, nil, nil, fmt.Errorf("pub witness error: %w", err) | ||
} | ||
if err = groth16.Verify(proof, vk, publicWitness, stdgroth16.GetNativeVerifierOptions(outer, field)); err != nil { | ||
return nil, nil, nil, nil, fmt.Errorf("verify error: %w", err) | ||
} | ||
return ccs, publicWitness, proof, vk, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package aggregator | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/consensys/gnark-crypto/ecc" | ||
"github.com/consensys/gnark/frontend" | ||
"github.com/consensys/gnark/frontend/cs/r1cs" | ||
"github.com/consensys/gnark/std/algebra/native/sw_bls12377" | ||
stdgroth16 "github.com/consensys/gnark/std/recursion/groth16" | ||
qt "github.com/frankban/quicktest" | ||
"github.com/vocdoni/vocdoni-z-sandbox/circuits/ballotproof" | ||
"github.com/vocdoni/vocdoni-z-sandbox/circuits/voteverifier" | ||
) | ||
|
||
func TestSameCircuitsInfo(t *testing.T) { | ||
c := qt.New(t) | ||
|
||
// generate users accounts and census | ||
privKey, pubKey, address, err := ballotproof.GenECDSAaccountForTest() | ||
c.Assert(err, qt.IsNil) | ||
vvData := []voteverifier.VoterTestData{{ | ||
PrivKey: privKey, | ||
PubKey: pubKey, | ||
Address: address, | ||
}} | ||
_, vvPlaceholder, _, err := voteverifier.GenInputsForTest(vvData, nil) | ||
c.Assert(err, qt.IsNil) | ||
|
||
mainCCS, err := frontend.Compile(ecc.BLS12_377.ScalarField(), r1cs.NewBuilder, &vvPlaceholder) | ||
c.Assert(err, qt.IsNil) | ||
mainVk := stdgroth16.PlaceholderVerifyingKey[sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT](mainCCS) | ||
|
||
dummyCCS, err := frontend.Compile(ecc.BLS12_377.ScalarField(), r1cs.NewBuilder, DummyPlaceholder(mainCCS)) | ||
c.Assert(err, qt.IsNil) | ||
dummyVk := stdgroth16.PlaceholderVerifyingKey[sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT](dummyCCS) | ||
|
||
c.Log("len(G1.K)", len(mainVk.G1.K)) | ||
c.Log("len(CommitmentKeys)", len(mainVk.CommitmentKeys)) | ||
c.Log("PublicAndCommitmentCommitted", mainVk.PublicAndCommitmentCommitted) | ||
|
||
c.Assert(dummyVk.G1.K, qt.HasLen, len(mainVk.G1.K), qt.Commentf("G1.K %d vs %d", len(dummyVk.G1.K), len(mainVk.G1.K))) | ||
c.Assert(dummyVk.CommitmentKeys, qt.HasLen, len(mainVk.CommitmentKeys), qt.Commentf("CommitmentKeys %d vs %d", len(dummyVk.CommitmentKeys), len(mainVk.CommitmentKeys))) | ||
c.Assert(dummyVk.PublicAndCommitmentCommitted, qt.ContentEquals, mainVk.PublicAndCommitmentCommitted, qt.Commentf("PublicAndCommitmentCommitted %v vs %v", dummyVk.PublicAndCommitmentCommitted, mainVk.PublicAndCommitmentCommitted)) | ||
} |
Oops, something went wrong.