Skip to content
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
4 changes: 2 additions & 2 deletions backend/groth16/bn254/solidity.go
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ contract Verifier {
{{- end }}

publicCommitments[{{$i}}] = uint256(
sha256(
{{ hashFnName }}(
abi.encodePacked(
commitments[{{mul $i 2}}],
commitments[{{sum (mul $i 2) 1}}],
Expand Down Expand Up @@ -713,7 +713,7 @@ contract Verifier {
{{- end }}

publicCommitments[{{$i}}] = uint256(
sha256(
{{ hashFnName }}(
abi.encodePacked(
commitments[{{mul $i 2}}],
commitments[{{sum (mul $i 2) 1}}],
Expand Down
38 changes: 32 additions & 6 deletions backend/groth16/bn254/verify.go

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

3 changes: 3 additions & 0 deletions backend/plonk/bn254/verify.go

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

76 changes: 75 additions & 1 deletion backend/solidity/solidity.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package solidity

import (
"fmt"
"hash"

"github.com/consensys/gnark/backend"
"golang.org/x/crypto/sha3"
)

// ExportOption defines option for altering the behavior of the prover in
// Prove, ReadAndProve and IsSolved methods. See the descriptions of functions
// returning instances of this type for implemented options.
Expand All @@ -8,13 +16,15 @@ type ExportOption func(*ExportConfig) error
// ExportConfig is the configuration for the prover with the options applied.
type ExportConfig struct {
PragmaVersion string
HashToFieldFn hash.Hash
}

// NewExportConfig returns a default ExportConfig with given export options opts
// applied.
func NewExportConfig(opts ...ExportOption) (ExportConfig, error) {
config := ExportConfig{
PragmaVersion: "0.8.24",
// we set default pragma version to 0.8.0+ to avoid needing to sync Solidity CI all the time
PragmaVersion: "^0.8.0",
}
for _, option := range opts {
if err := option(&config); err != nil {
Expand All @@ -31,3 +41,67 @@ func WithPragmaVersion(version string) ExportOption {
return nil
}
}

// WithHashToFieldFunction changes the hash function used for hashing
// bytes to field. If not set then the default hash function based on RFC 9380
// is used. Used mainly for compatibility between different systems and
// efficient recursion.
func WithHashToFieldFunction(hFunc hash.Hash) ExportOption {
return func(cfg *ExportConfig) error {
cfg.HashToFieldFn = hFunc
return nil
}
}

// WithProverTargetSolidityVerifier returns a prover option that sets all the
// necessary prover options which are suitable for verifying the proofs in the
// Solidity verifier.
//
// For PLONK this is a no-op option as the Solidity verifier is directly
// compatible with the default prover options. Regardless, it is recommended to
// use this option for consistency and possible future changes in the Solidity
// verifier.
//
// For Groth16 this option sets the hash function used for hashing bytes to
// field to [sha3.NewLegacyKeccak256] as the Solidity verifier does not support
// the standard hash-to-field function. We use legacy Keccak256 in Solidity for
// the cheapest gas usage.
func WithProverTargetSolidityVerifier(bid backend.ID) backend.ProverOption {
switch bid {
case backend.GROTH16:
// Solidity verifier does not support standard hash-to-field function.
// Choose efficient one.
return backend.WithProverHashToFieldFunction(sha3.NewLegacyKeccak256())
case backend.PLONK:
// default hash function works for PLONK. We just have to return a no-op option
return func(*backend.ProverConfig) error {
return nil
}
default:
return func(*backend.ProverConfig) error {
return fmt.Errorf("unsupported backend ID: %s", bid)
}
}
}

// WithVerifierTargetSolidityVerifier returns a verifier option that sets all
// the necessary verifier options which are suitable for verifying the proofs
// targeted for the Solidity verifier. See the comments in
// [WithProverTargetSolidityVerifier].
func WithVerifierTargetSolidityVerifier(bid backend.ID) backend.VerifierOption {
switch bid {
case backend.GROTH16:
// Solidity verifier does not support standard hash-to-field function.
// Choose efficient one.
return backend.WithVerifierHashToFieldFunction(sha3.NewLegacyKeccak256())
case backend.PLONK:
// default hash function works for PLONK. We just have to return a no-op option
return func(*backend.VerifierConfig) error {
return nil
}
default:
return func(*backend.VerifierConfig) error {
return fmt.Errorf("unsupported backend ID: %s", bid)
}
}
}
176 changes: 176 additions & 0 deletions backend/solidity/solidity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package solidity_test

import (
"crypto/sha256"
"fmt"
"hash"
"testing"

"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend"
"github.com/consensys/gnark/backend/solidity"
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/test"
"golang.org/x/crypto/sha3"
)

type noCommitCircuit struct {
A, B, Out frontend.Variable `gnark:",public"`
}

func (c *noCommitCircuit) Define(api frontend.API) error {
res := api.Mul(c.A, c.B)
api.AssertIsEqual(res, c.Out)
return nil
}

type commitCircuit struct {
A, B, Out frontend.Variable `gnark:",public"`
}

func (c *commitCircuit) Define(api frontend.API) error {
res := api.Mul(c.A, c.B)
api.AssertIsEqual(res, c.Out)
cmter, ok := api.(frontend.Committer)
if !ok {
return fmt.Errorf("api does not support commitment")
}
cmt1, err := cmter.Commit(res)
if err != nil {
return err
}
api.AssertIsDifferent(cmt1, res)
return nil
}

type twoCommitCircuit struct {
A, B, Out frontend.Variable `gnark:",public"`
}

func (c *twoCommitCircuit) Define(api frontend.API) error {
res := api.Mul(c.A, c.B)
api.AssertIsEqual(res, c.Out)
cmter, ok := api.(frontend.Committer)
if !ok {
return fmt.Errorf("api does not support commitment")
}
cmt1, err := cmter.Commit(res)
if err != nil {
return err
}
cmt2, err := cmter.Commit(cmt1)
if err != nil {
return err
}
api.AssertIsDifferent(cmt1, cmt2)
return nil
}

func TestNoCommitment(t *testing.T) {
// should succeed both with G16 and PLONK:
assert := test.NewAssert(t)
circuit := &noCommitCircuit{}
assignment := &noCommitCircuit{A: 2, B: 3, Out: 6}
defaultOpts := []test.TestingOption{
test.WithCurves(ecc.BN254),
test.WithValidAssignment(assignment),
}
checkCircuit := func(assert *test.Assert, bid backend.ID) {
opts := append(defaultOpts,
test.WithBackends(bid),
)

assert.CheckCircuit(circuit, opts...)
}
assert.Run(func(assert *test.Assert) {
checkCircuit(assert, backend.GROTH16)
}, "Groth16")
assert.Run(func(assert *test.Assert) {
checkCircuit(assert, backend.PLONK)
}, "PLONK")
}

func TestSingleCommitment(t *testing.T) {
// should succeed both with G16 and PLONK:
// - But for G16 only if the hash-to-field is set to a supported one.
// - but for PLONK only if the hash-to-field is the default one. If not, then it should fail.
assert := test.NewAssert(t)
circuit := &commitCircuit{}
assignment := &commitCircuit{A: 2, B: 3, Out: 6}
defaultOpts := []test.TestingOption{
test.WithCurves(ecc.BN254),
test.WithValidAssignment(assignment),
}
checkCircuit := func(assert *test.Assert, bid backend.ID, newHash func() hash.Hash) {
opts := append(defaultOpts,
test.WithBackends(bid),
test.WithProverOpts(
backend.WithProverHashToFieldFunction(newHash()),
),
test.WithVerifierOpts(
backend.WithVerifierHashToFieldFunction(newHash()),
),
test.WithSolidityExportOptions(solidity.WithHashToFieldFunction(newHash())),
)

assert.CheckCircuit(circuit, opts...)
}
// G16 success with explicitly set options
assert.Run(func(assert *test.Assert) {
checkCircuit(assert, backend.GROTH16, sha256.New)
}, "groth16", "sha256")
assert.Run(func(assert *test.Assert) {
checkCircuit(assert, backend.GROTH16, sha3.NewLegacyKeccak256)
}, "groth16", "keccak256")
// G16 success with using TargetSolidityVerifier
assert.Run(func(assert *test.Assert) {
opts := append(defaultOpts,
test.WithBackends(backend.GROTH16),
test.WithProverOpts(
solidity.WithProverTargetSolidityVerifier(backend.GROTH16),
),
test.WithVerifierOpts(
solidity.WithVerifierTargetSolidityVerifier(backend.GROTH16),
),
)
assert.CheckCircuit(circuit, opts...)
}, "groth16", "targetSolidityVerifier")
// G16 success without any options because we set default options already in
// assert.CheckCircuit if they are not set.
assert.Run(func(assert *test.Assert) {
opts := append(defaultOpts,
test.WithBackends(backend.GROTH16),
)
assert.CheckCircuit(circuit, opts...)
}, "groth16", "no-options")

// PLONK success with default options
assert.Run(func(assert *test.Assert) {
opts := append(defaultOpts,
test.WithBackends(backend.PLONK),
)
assert.CheckCircuit(circuit, opts...)
}, "plonk", "default")
// PLONK success with using TargetSolidityVerifier
assert.Run(func(assert *test.Assert) {
opts := append(defaultOpts,
test.WithBackends(backend.PLONK),
test.WithProverOpts(
solidity.WithProverTargetSolidityVerifier(backend.PLONK),
),
test.WithVerifierOpts(
solidity.WithVerifierTargetSolidityVerifier(backend.PLONK),
),
)
assert.CheckCircuit(circuit, opts...)
}, "plonk", "targetSolidityVerifier")
}

func TestTwoCommitments(t *testing.T) {
// should succeed with PLONK only.
// - but for PLONK only if the hash-to-field is the default one. If not, then it should fail.
assert := test.NewAssert(t)
circuit := &twoCommitCircuit{}
assignment := &twoCommitCircuit{A: 2, B: 3, Out: 6}
assert.CheckCircuit(circuit, test.WithCurves(ecc.BN254), test.WithValidAssignment(assignment), test.WithBackends(backend.PLONK))
}
Loading