Skip to content
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

feat: Groth16 Solidity contract with commitments #1063

Merged
merged 17 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
351 changes: 294 additions & 57 deletions backend/groth16/bn254/solidity.go

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions backend/groth16/bn254/verify.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 0 additions & 6 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"sort"
"testing"

"github.com/consensys/gnark/backend"
"github.com/consensys/gnark/constraint/solver"
"github.com/consensys/gnark/internal/backend/circuits"
"github.com/consensys/gnark/test"
Expand Down Expand Up @@ -63,11 +62,6 @@ func TestIntegrationAPI(t *testing.T) {
opts = append(opts, test.NoFuzzing())
}

if name == "commit" && test.SolcCheck {
// TODO @gbotrel FIXME groth16 solidity verifier needs updating.
opts = append(opts, test.WithBackends(backend.PLONK))
}

assert.CheckCircuit(tData.Circuit, opts...)
}, name)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/backend/circuits/circuits.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func addEntry(name string, circuit, proverGood, proverBad frontend.Circuit, curv
panic("name " + name + "already taken by another test circuit ")
}

Circuits[name] = TestCircuit{circuit, []frontend.Circuit{proverGood}, []frontend.Circuit{proverBad}, nil, curves}
Circuits[name] = TestCircuit{Circuit: circuit, ValidAssignments: []frontend.Circuit{proverGood}, InvalidAssignments: []frontend.Circuit{proverBad}, HintFunctions: nil, Curves: curves}
}

func addNewEntry(name string, circuit frontend.Circuit, proverGood, proverBad []frontend.Circuit, curves []ecc.ID, hintFunctions ...solver.Hint) {
Expand All @@ -39,5 +39,5 @@ func addNewEntry(name string, circuit frontend.Circuit, proverGood, proverBad []
}
solver.RegisterHint(hintFunctions...)

Circuits[name] = TestCircuit{circuit, proverGood, proverBad, hintFunctions, curves}
Circuits[name] = TestCircuit{Circuit: circuit, ValidAssignments: proverGood, InvalidAssignments: proverBad, HintFunctions: nil, Curves: curves}
ivokub marked this conversation as resolved.
Show resolved Hide resolved
}
39 changes: 29 additions & 10 deletions internal/backend/circuits/commit.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package circuits

import "github.com/consensys/gnark/frontend"
import (
"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark-crypto/ecc/bn254"
"github.com/consensys/gnark/frontend"
)

type commitCircuit struct {
Public frontend.Variable `gnark:",public"`
Expand All @@ -23,15 +27,30 @@ func (circuit *commitCircuit) Define(api frontend.API) error {
return nil
}

func init() {

var circuit, good, bad commitCircuit

good.X = 3
good.Public = 16
type noCommitCircuit struct {
Public frontend.Variable `gnark:",public"`
X frontend.Variable
}

bad.X = 4
bad.Public = 0
func (circuit *noCommitCircuit) Define(api frontend.API) error {
api.AssertIsDifferent(circuit.Public, 0)
a := api.Mul(circuit.X, circuit.X)
for i := 0; i < 10; i++ {
a = api.Mul(a, circuit.X)
}
c := api.Add(a, circuit.X)
api.AssertIsDifferent(c, a)
return nil
}

addEntry("commit", &circuit, &good, &bad, nil)
func init() {
// need to have separate test cases as the hash-to-field for PLONK and Groth16 verifiers are different
addEntry(
"commit",
&commitCircuit{}, &commitCircuit{Public: 16, X: 3}, &commitCircuit{Public: 0, X: 4},
[]ecc.ID{bn254.ID})
addEntry(
"no_commit",
&noCommitCircuit{}, &noCommitCircuit{Public: 16, X: 3}, &noCommitCircuit{Public: 0, X: 4},
[]ecc.ID{bn254.ID})
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,14 @@ func Verify(proof *Proof, vk *VerifyingKey, publicWitness fr.Vector, opts ...bac
// compute e(Σx.[Kvk(t)]1, -[γ]2)
var kSum curve.G1Jac
if _, err := kSum.MultiExp(vk.G1.K[1:], publicWitness, ecc.MultiExpConfig{}); err != nil {
return err
return err
}
kSum.AddMixed(&vk.G1.K[0])

for i := range proof.Commitments {
kSum.AddMixed(&proof.Commitments[i])
}

var kSumAff curve.G1Affine
kSumAff.FromJacobian(&kSum)

Expand All @@ -112,7 +112,7 @@ func Verify(proof *Proof, vk *VerifyingKey, publicWitness fr.Vector, opts ...bac

// wait for (eKrsδ, eArBs)
if err := <-chDone; err != nil {
return err
return err
}

right = curve.FinalExponentiation(&right, &doubleML)
Expand All @@ -128,10 +128,13 @@ func Verify(proof *Proof, vk *VerifyingKey, publicWitness fr.Vector, opts ...bac
{{if eq .Curve "BN254"}}
// ExportSolidity writes a solidity Verifier contract on provided writer.
// This is an experimental feature and gnark solidity generator as not been thoroughly tested.
//
//
// See https://github.com/ConsenSys/gnark-tests for example usage.
func (vk *VerifyingKey) ExportSolidity(w io.Writer) error {
helpers := template.FuncMap{
"sum": func(a, b int) int {
return a + b
},
"sub": func(a, b int) int {
return a - b
},
Expand All @@ -147,6 +150,13 @@ func (vk *VerifyingKey) ExportSolidity(w io.Writer) error {
},
}

log := logger.Logger()
if len(vk.PublicAndCommitmentCommitted) > 1 {
log.Warn().Msg("exporting solidity verifier with more than one commitment is not supported")
} else if len(vk.PublicAndCommitmentCommitted) == 1 {
log.Warn().Msg("exporting solidity verifier only supports `sha256` as `HashToField`. The generated contract may not work for proofs generated with other hash functions.")
}

tmpl, err := template.New("").Funcs(helpers).Parse(solidityTemplate)
if err != nil {
return err
Expand Down
8 changes: 4 additions & 4 deletions std/math/polynomial/polynomial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func testEvalPoly[FR emulated.FieldParams](t *testing.T, p []int64, at int64, ev
Evaluation: emulated.ValueOf[FR](evaluation),
}

assert.CheckCircuit(&evalPolyCircuit[FR]{P: make([]emulated.Element[FR], len(p))}, test.WithValidAssignment(&witness))
assert.CheckCircuit(&evalPolyCircuit[FR]{P: make([]emulated.Element[FR], len(p))}, test.WithValidAssignment(&witness), test.NoSolidityChecks())
}

func TestEvalPoly(t *testing.T) {
Expand Down Expand Up @@ -98,7 +98,7 @@ func testEvalMultiLin[FR emulated.FieldParams](t *testing.T) {
Evaluation: emulated.ValueOf[FR](17),
}

assert.CheckCircuit(&evalMultiLinCircuit[FR]{M: make([]emulated.Element[FR], 4), At: make([]emulated.Element[FR], 2)}, test.WithValidAssignment(&witness))
assert.CheckCircuit(&evalMultiLinCircuit[FR]{M: make([]emulated.Element[FR], 4), At: make([]emulated.Element[FR], 2)}, test.WithValidAssignment(&witness), test.NoSolidityChecks())
}

type evalEqCircuit[FR emulated.FieldParams] struct {
Expand Down Expand Up @@ -144,7 +144,7 @@ func testEvalEq[FR emulated.FieldParams](t *testing.T) {
Eq: emulated.ValueOf[FR](148665),
}

assert.CheckCircuit(&evalEqCircuit[FR]{X: make([]emulated.Element[FR], 4), Y: make([]emulated.Element[FR], 4)}, test.WithValidAssignment(&witness))
assert.CheckCircuit(&evalEqCircuit[FR]{X: make([]emulated.Element[FR], 4), Y: make([]emulated.Element[FR], 4)}, test.WithValidAssignment(&witness), test.NoSolidityChecks())
}

type interpolateLDECircuit[FR emulated.FieldParams] struct {
Expand Down Expand Up @@ -180,7 +180,7 @@ func testInterpolateLDE[FR emulated.FieldParams](t *testing.T, at int64, values
Expected: emulated.ValueOf[FR](expected),
}

assert.CheckCircuit(&interpolateLDECircuit[FR]{Values: make([]emulated.Element[FR], len(values))}, test.WithValidAssignment(assignment))
assert.CheckCircuit(&interpolateLDECircuit[FR]{Values: make([]emulated.Element[FR], len(values))}, test.WithValidAssignment(assignment), test.NoSolidityChecks())
}

func TestInterpolateLDEOnRange(t *testing.T) {
Expand Down
15 changes: 13 additions & 2 deletions test/assert_checkcircuit.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package test

import (
"crypto/sha256"

"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend"
"github.com/consensys/gnark/backend/groth16"
Expand Down Expand Up @@ -120,10 +122,19 @@ func (assert *Assert) CheckCircuit(circuit frontend.Circuit, opts ...TestingOpti
w := w
assert.Run(func(assert *Assert) {
checkSolidity := opt.checkSolidity && curve == ecc.BN254
proof, err := concreteBackend.prove(ccs, pk, w.full, opt.proverOpts...)
proverOpts := opt.proverOpts
verifierOpts := opt.verifierOpts
if b == backend.GROTH16 {
// currently groth16 Solidity checker only supports circuits with up to 1 commitment
checkSolidity = checkSolidity && (len(ccs.GetCommitments().CommitmentIndexes()) <= 1)
// additionally, we use sha256 as hash to field (fixed in Solidity contract)
proverOpts = append(proverOpts, backend.WithProverHashToFieldFunction(sha256.New()))
verifierOpts = append(verifierOpts, backend.WithVerifierHashToFieldFunction(sha256.New()))
}
proof, err := concreteBackend.prove(ccs, pk, w.full, proverOpts...)
assert.noError(err, &w)

err = concreteBackend.verify(proof, vk, w.public, opt.verifierOpts...)
err = concreteBackend.verify(proof, vk, w.public, verifierOpts...)
assert.noError(err, &w)

if checkSolidity {
Expand Down
51 changes: 25 additions & 26 deletions test/assert_solidity.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package test

import (
"bytes"
"encoding/hex"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"

fr_bn254 "github.com/consensys/gnark-crypto/ecc/bn254/fr"
"github.com/consensys/gnark/backend"
groth16_bn254 "github.com/consensys/gnark/backend/groth16/bn254"
plonk_bn254 "github.com/consensys/gnark/backend/plonk/bn254"
"github.com/consensys/gnark/backend/witness"
)

Expand All @@ -26,7 +24,7 @@ type verifyingKey interface {
func (assert *Assert) solidityVerification(b backend.ID, vk verifyingKey,
proof any,
validPublicWitness witness.Witness) {
if !SolcCheck || vk.NbPublicWitness() == 0 {
if !SolcCheck || len(validPublicWitness.Vector().(fr_bn254.Vector)) == 0 {
return // nothing to check, will make solc fail.
}
assert.t.Helper()
Expand All @@ -53,29 +51,30 @@ func (assert *Assert) solidityVerification(b backend.ID, vk verifyingKey,
out, err := cmd.CombinedOutput()
assert.NoError(err, string(out))

// proof to hex
var proofStr string
var optBackend string
// len(vk.K) - 1 == len(publicWitness) + len(commitments)
numOfCommitments := vk.NbPublicWitness() - len(validPublicWitness.Vector().(fr_bn254.Vector))

checkerOpts := []string{"verify"}
if b == backend.GROTH16 {
optBackend = "--groth16"
var buf bytes.Buffer
_proof := proof.(*groth16_bn254.Proof)
_, err = _proof.WriteRawTo(&buf)
assert.NoError(err)
proofBytes := buf.Bytes()
// keep only fpSize * 8 bytes; for now solidity contract doesn't handle the commitment part.
proofBytes = proofBytes[:32*8]
proofStr = hex.EncodeToString(proofBytes)
checkerOpts = append(checkerOpts, "--groth16")
} else if b == backend.PLONK {
optBackend = "--plonk"
_proof := proof.(*plonk_bn254.Proof)
// TODO @gbotrel make a single Marshal function for PlonK proof.
proofStr = hex.EncodeToString(_proof.MarshalSolidity())
checkerOpts = append(checkerOpts, "--plonk")
} else {
panic("not implemented")
}

// proof to hex
_proof, ok := proof.(interface{ MarshalSolidity() []byte })
if !ok {
panic("proof does not implement MarshalSolidity()")
}

proofStr := hex.EncodeToString(_proof.MarshalSolidity())

if numOfCommitments > 0 {
checkerOpts = append(checkerOpts, "--commitment", strconv.Itoa(numOfCommitments))
}

// public witness to hex
bPublicWitness, err := validPublicWitness.MarshalBinary()
assert.NoError(err)
Expand All @@ -86,14 +85,14 @@ func (assert *Assert) solidityVerification(b backend.ID, vk verifyingKey,
bPublicWitness = bPublicWitness[12:]
publicWitnessStr := hex.EncodeToString(bPublicWitness)

checkerOpts = append(checkerOpts, "--dir", tmpDir)
checkerOpts = append(checkerOpts, "--nb-public-inputs", strconv.Itoa(len(validPublicWitness.Vector().(fr_bn254.Vector))))
checkerOpts = append(checkerOpts, "--proof", proofStr)
checkerOpts = append(checkerOpts, "--public-inputs", publicWitnessStr)

// verify proof
// gnark-solidity-checker verify --dir tmdir --groth16 --nb-public-inputs 1 --proof 1234 --public-inputs dead
cmd = exec.Command("gnark-solidity-checker", "verify",
"--dir", tmpDir,
optBackend,
"--nb-public-inputs", strconv.Itoa(vk.NbPublicWitness()),
"--proof", proofStr,
"--public-inputs", publicWitnessStr)
cmd = exec.Command("gnark-solidity-checker", checkerOpts...)
assert.t.Log("running ", cmd.String())
out, err = cmd.CombinedOutput()
assert.NoError(err, string(out))
Expand Down
7 changes: 4 additions & 3 deletions test/commitments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,12 @@ func init() {

func TestCommitment(t *testing.T) {
t.Parallel()

assert := NewAssert(t)

for i, assignment := range commitmentTestCircuits {
t.Log("circuit", i, removePackageName(reflect.TypeOf(assignment).String()))
assert.CheckCircuit(hollow(assignment), WithValidAssignment(assignment), WithBackends(backend.GROTH16, backend.PLONK))
assert.Run(func(assert *Assert) {
assert.CheckCircuit(hollow(assignment), WithValidAssignment(assignment), WithBackends(backend.GROTH16, backend.PLONK))
}, fmt.Sprintf("%d-%s", i, removePackageName(reflect.TypeOf(assignment).String())))
}
}

Expand Down
Loading