Skip to content

Commit

Permalink
feature: aggregate votes circuit (#5)
Browse files Browse the repository at this point in the history
* 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
lucasmenendez authored Dec 30, 2024
1 parent dd7d1b7 commit 41d1e4a
Show file tree
Hide file tree
Showing 20 changed files with 1,194 additions and 481 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ jobs:
env:
GORACE: atexit_sleep_ms=10 # the default of 1000 makes every Go package test sleep for 1s; see https://go.dev/issues/20364
run: go test ./...
-race -timeout=15m -vet=off
-race -timeout=1h -vet=off
-cover -coverpkg=./... -covermode=atomic -args -test.gocoverdir="$PWD/gocoverage-unit/"
- name: Run Go test
if: steps.go-test-race.outcome == 'skipped'
# quicker, non-race test in case it's a PR or push to dev
run: go test ./...
run: go test ./... -timeout=1h
125 changes: 125 additions & 0 deletions circuits/aggregator/aggregator.go
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
}
34 changes: 34 additions & 0 deletions circuits/aggregator/aggregator_test.go
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())
}
44 changes: 44 additions & 0 deletions circuits/aggregator/dummy.go
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}
}
112 changes: 112 additions & 0 deletions circuits/aggregator/dummy_helpers.go
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
}
45 changes: 45 additions & 0 deletions circuits/aggregator/dummy_test.go
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))
}
Loading

0 comments on commit 41d1e4a

Please sign in to comment.