neoneo-go/pkg/smartcontract/zkpbinding/binding.go
Anna Shaleva e2abb5cb7c zkpbinding: update templates of go.sum and go.mod for Verifier contract
Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
2023-10-05 13:46:05 +03:00

384 lines
16 KiB
Go

// Package zkpbinding contains a set of helper functions aimed to generate and
// interact with Verifier smart contract written in Go and using Groth-16 proving
// system over BLS12-381 elliptic curve to verify proofs. Package zkpbinding
// provides the Veifier contract generation functionality itself as far as a
// helper that converts groth16.Proof to the Verifier-specific set of arguments.
//
// Please, check out the example of zkpbinding package usage to generate and
// verify proofs on the Neo chain:
// https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/smartcontract/zkpbinding
package zkpbinding
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"text/template"
"github.com/consensys/gnark-crypto/ecc"
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
"github.com/consensys/gnark/backend/groth16"
"github.com/consensys/gnark/backend/witness"
"github.com/nspcc-dev/neo-go/pkg/util/slice"
)
// Config represents a configuration for Verifier Go smart contract generator.
type Config struct {
// VerifyingKey must be a Groth-16 BLS12-381 specific verifier key,
// parameters of which will be used to generate Verifier Neo smart contract.
VerifyingKey groth16.VerifyingKey
// Output is a writer for the resulting Verifier Go smart contract, it must
// not be nil.
Output io.Writer
// CfgOutput is a writer for the resulting Verifier Go smart contract YAML
// configuration file needed to compile the contract. It may be nil if the
// contract configuration file generation should be omitted.
CfgOutput io.Writer
// GomodOutput is a writer for the resulting go.mod file of the Verifier Go
// smart contract needed to compile it. It may be nil if the go.mod file
// generation should be omitted.
GomodOutput io.Writer
// GosumOutput is a writer for the resulting go.sum file of the Verifier Go
// smart contract needed to compile it. It may be nil if the go.sum file
// generation should be omitted.
GosumOutput io.Writer
}
// A set of Verifier smart contract template related constants.
const (
// goVerificationTmpl is a verification smart contract template. It contains
// a single `verifyProof` method that accepts a proof represented as three
// BLS12-381 curve points and public information required for verification
// represented as a list of serialized 32-bytes field elements in the LE form.
// The boolean result of `verifyProof` is either `true` (if the proof is
// valid) or `false` (if the proof is invalid). The smart contract generated
// from this template can be immediately compiled without any additional
// changes using NeoGo compiler, deployed to the Neo chain and invoked. The
// verification contract is circuit-specific, i.e. corresponds to a specific
// single constraint system. Thus, every new circuit requires vew verification
// contract to be generated and deployed to the chain.
goVerificationTmpl = `// Package main contains verification smart contract that uses Neo BLS12-381
// curves interoperability functionality to verify provided proof against provided
// public input. The contract contains a single 'verifyProof' method that accepts
// a proof represented as three BLS12-381 curve points and public witnesses
// required for verification represented as a list of serialized 32-bytes field
// elements in the LE form. This contract is circuit-specific and can not be used
// to verify other circuits.
//
// Use NeoGo smart contract compiler to compile this contract:
// https://github.com/nspcc-dev/neo-go/blob/master/docs/compiler.md#compiling.
// You will need to create contract YAML configuration file and proper go.mod and
// go.sum files required for compilation. Please, refer to the NeoGo ZKP example
// to see how to verify proofs via the Verifier contract:
// https://github.com/nspcc-dev/neo-go/tree/master/examples/zkp/cubic_circuit.
//
// This contract is automatically generated.
package main
import (
"github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
"github.com/nspcc-dev/neo-go/pkg/interop/util"
)
// A set of circuit-specific variables required for verification. Should be generated
// using MPC process.
var (
// G1 Affine point.
alpha = []byte{{ byteSliceToStr .Alpha }}
// G2 Affine point.
beta = []byte{{ byteSliceToStr .Beta }}
// G2 Affine point.
gamma = []byte{{ byteSliceToStr .Gamma }}
// G2 Affine point.
delta = []byte{{ byteSliceToStr .Delta }}
// A set of G1 Affine points.
ic = [][]byte{
{{- range $i := .ICs }}
{{ byteSliceToStr $i }},{{ end -}}
}
)
// VerifyProof verifies the given proof represented as three serialized compressed
// BLS12-381 points against the public information represented as a list of
// serialized 32-bytes field elements in the LE form. Verification process
// follows the Groth-16 proving system and is taken from the
// https://github.com/neo-project/neo/issues/2647#issuecomment-1002893109 without
// any changes. Verification process checks the following equality:
//
// A * B = alpha * beta + sum(pub_input[i] * (beta * u_i(x) + alpha * v_i(x) + w_i(x)) / gamma) * gamma + C * delta
func VerifyProof(a []byte, b []byte, c []byte, publicInput [][]byte) bool {
alphaPoint := crypto.Bls12381Deserialize(alpha)
betaPoint := crypto.Bls12381Deserialize(beta)
gammaPoint := crypto.Bls12381Deserialize(gamma)
deltaPoint := crypto.Bls12381Deserialize(delta)
aPoint := crypto.Bls12381Deserialize(a)
bPoint := crypto.Bls12381Deserialize(b)
cPoint := crypto.Bls12381Deserialize(c)
// Equation left1: A*B
lt := crypto.Bls12381Pairing(aPoint, bPoint)
// Equation right1: alpha*beta
rt1 := crypto.Bls12381Pairing(alphaPoint, betaPoint)
// Equation right2: sum(pub_input[i]*(beta*u_i(x)+alpha*v_i(x)+w_i(x))/gamma)*gamma
inputlen := len(publicInput)
iclen := len(ic)
if iclen != inputlen+1 {
panic("error: inputlen or iclen")
}
icPoints := make([]crypto.Bls12381Point, iclen)
for i := 0; i < iclen; i++ {
icPoints[i] = crypto.Bls12381Deserialize(ic[i])
}
acc := icPoints[0]
for i := 0; i < inputlen; i++ {
scalar := publicInput[i] // 32-bytes LE field element.
temp := crypto.Bls12381Mul(icPoints[i+1], scalar, false)
acc = crypto.Bls12381Add(acc, temp)
}
rt2 := crypto.Bls12381Pairing(acc, gammaPoint)
// Equation right3: C*delta
rt3 := crypto.Bls12381Pairing(cPoint, deltaPoint)
// Check equality.
t1 := crypto.Bls12381Add(rt1, rt2)
t2 := crypto.Bls12381Add(t1, rt3)
return util.Equals(lt, t2)
}
`
// verifyCfg is a contract configuration file required to compile smart
// contract.
verifyCfg = `name: "Groth-16 Verifier contract"
sourceurl: https://github.com/nspcc-dev/neo-go/
supportedstandards: []`
// verifyGomod is a standard go.mod file containing module name, go version
// and dependency packages version needed for smart contract compilation.
verifyGomod = `module verify
go 1.18
require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231004150345-8849ccde2524
`
// verifyGosum is a standard go.sum file needed for contract compilation.
verifyGosum = `github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231004150345-8849ccde2524 h1:LKp/89ftf+MwMExKgnbwjQp5zQTUZ3lDCc+DZ4VeSRc=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231004150345-8849ccde2524/go.mod h1:ZUuXOkdtHZgaC13za/zMgXfQFncZ0jLzfQTe+OsDOtg=
`
)
// verifyingKeyConstantPartLen is the length of the constant-len part of a
// serialized compressed verifying key. It is used to check that serialization
// format of a verifying key is expected.
const verifyingKeyConstantPartLen = bls12381.SizeOfG1AffineCompressed + bls12381.SizeOfG1AffineCompressed + // [α]1,[β]1
bls12381.SizeOfG2AffineCompressed + bls12381.SizeOfG2AffineCompressed + // [β]2,[γ]2
bls12381.SizeOfG1AffineCompressed + bls12381.SizeOfG2AffineCompressed + // [δ]1,[δ]2
4 // len(Kvk) in BE (a number of public wires for this circuit)
// VerifyProofArgs is the set of arguments of `verifyProof` method of a
// Verifier contract in serialized form (as the contract accepts them).
type VerifyProofArgs struct {
A []byte
B []byte
C []byte
PublicWitnesses []any
}
// tmplParams is a set of parameters used by verification contract template.
type tmplParams struct {
Alpha []byte
Beta []byte
Gamma []byte
Delta []byte
ICs [][]byte
}
// GenerateVerifier generates a Verifier smart contract written in Go for Neo
// blockchain. The contract contains a single `verifyProof` method that accepts
// a proof represented as three BLS12-381 curve points and public witnesses
// required for verification represented as a list of serialized 32-bytes field
// elements in the LE form. The boolean result of `verifyProof` is either `true`
// (if the proof is valid) or `false` (if the proof is invalid). The smart
// contract generated from this template can be immediately compiled without
// any additional changes using NeoGo compiler, deployed to the Neo chain and
// invoked. The verification contract is circuit-specific, i.e. corresponds to
// a specific constraint system. Thus, every new circuit requires its own
// verification contract to be generated and deployed to the chain.
//
// GenerateVerifier also generates a proper contract YAML configuration file,
// go.mod and go.sum files if the corresponding writers are provided via cfg.
func GenerateVerifier(cfg Config) error {
if cfg.VerifyingKey == nil {
return fmt.Errorf("nil verifying key")
}
if cfg.VerifyingKey.CurveID() != ecc.BLS12_381 {
return fmt.Errorf("unexpected elliptic curve: %s", cfg.VerifyingKey.CurveID())
}
// Fetch the contract's public verification parameters. We can't directly access
// the VerifyingKey elements, because it's hidden under internal package, thus,
// take these parameters from the serialized VerifyingKey representation. It
// follows the bellman format:
// https://github.com/zkcrypto/bellman/blob/fa9be45588227a8c6ec34957de3f68705f07bd92/src/groth16/mod.rs#L143
// [α]1,[β]1,[β]2,[γ]2,[δ]1,[δ]2,uint32(len(Kvk)),[Kvk]1
// See also the serialisation code: https://github.com/Consensys/gnark/blob/165b49ab88d69c97867a76e147e6fd41af138210/internal/backend/bls12-381/groth16/marshal.go#L95
var buf bytes.Buffer
_, err := cfg.VerifyingKey.WriteTo(&buf)
if err != nil {
return fmt.Errorf("failed to serialize verifying key: %w", err)
}
vkBytes := slice.Copy(buf.Bytes())
// Ensure the serialized verifier key has the expected length/format (just in case).
if len(vkBytes) < verifyingKeyConstantPartLen {
return errors.New("unexpected len of constant-size part of serialized verifying key")
}
kvkLen := binary.BigEndian.Uint32(vkBytes[verifyingKeyConstantPartLen-4:])
if len(vkBytes) < verifyingKeyConstantPartLen+int(kvkLen*bls12381.SizeOfG1AffineCompressed) {
return fmt.Errorf("unexpected len of serialized verifying key: expected at least %d got %d", verifyingKeyConstantPartLen+int(kvkLen*bls12381.SizeOfG1AffineCompressed), len(vkBytes))
}
var (
alphaG1Offset = 0
betaG2Offset = bls12381.SizeOfG1AffineCompressed + bls12381.SizeOfG1AffineCompressed // [α]1,[β]1
gammaG2Offset = bls12381.SizeOfG1AffineCompressed + bls12381.SizeOfG1AffineCompressed + // [α]1,[β]1
bls12381.SizeOfG2AffineCompressed // [β]2
deltaG2Offset = bls12381.SizeOfG1AffineCompressed + bls12381.SizeOfG1AffineCompressed + // [α]1,[β]1
bls12381.SizeOfG2AffineCompressed + bls12381.SizeOfG2AffineCompressed + // [β]2,[γ]2
bls12381.SizeOfG1AffineCompressed // [δ]1
kvkLenOffset = bls12381.SizeOfG1AffineCompressed + bls12381.SizeOfG1AffineCompressed + // [α]1,[β]1
bls12381.SizeOfG2AffineCompressed + bls12381.SizeOfG2AffineCompressed + // [β]2,[γ]2
bls12381.SizeOfG1AffineCompressed + bls12381.SizeOfG2AffineCompressed // [δ]1,[δ]2
kvkStartOffset = kvkLenOffset + 4
)
alphaG1 := vkBytes[alphaG1Offset : alphaG1Offset+bls12381.SizeOfG1AffineCompressed]
betaG2 := vkBytes[betaG2Offset : betaG2Offset+bls12381.SizeOfG2AffineCompressed]
gammaG2 := vkBytes[gammaG2Offset : gammaG2Offset+bls12381.SizeOfG2AffineCompressed]
deltaG2 := vkBytes[deltaG2Offset : deltaG2Offset+bls12381.SizeOfG2AffineCompressed]
kvks := make([][]byte, kvkLen)
for i := range kvks {
start := kvkStartOffset + i*bls12381.SizeOfG1AffineCompressed
end := start + bls12381.SizeOfG1AffineCompressed
kvks[i] = vkBytes[start:end]
}
// Generate verification contract from the template using the retrieved
// verification parameters.
tmpl := template.Must(template.New("generate").Funcs(template.FuncMap{
"byteSliceToStr": byteSliceToStr,
}).Parse(goVerificationTmpl))
err = tmpl.Execute(cfg.Output, tmplParams{
Alpha: alphaG1,
Beta: betaG2,
Gamma: gammaG2,
Delta: deltaG2,
ICs: kvks,
})
if err != nil {
return fmt.Errorf("failed to generate template: %w", err)
}
if cfg.CfgOutput != nil {
_, err = cfg.CfgOutput.Write([]byte(verifyCfg))
if err != nil {
return fmt.Errorf("failed to generate contract configuration file: %w", err)
}
}
if cfg.GomodOutput != nil {
_, err = cfg.GomodOutput.Write([]byte(verifyGomod))
if err != nil {
return fmt.Errorf("failed to generate go.mod file: %w", err)
}
}
if cfg.GosumOutput != nil {
_, err = cfg.GosumOutput.Write([]byte(verifyGosum))
if err != nil {
return fmt.Errorf("failed to generate go.mod file: %w", err)
}
}
return nil
}
// byteSliceToStr is a codegen helper that converts byte slice to a go-like slice.
func byteSliceToStr(s []byte) string {
var res string
for _, b := range s {
res += fmt.Sprintf("%d, ", b)
}
return `{` + res[:len(res)-2] + `}`
}
// GetVerifyProofArgs returns a serialized set of arguments `verifyProof` method
// of a generated Verifier contract accepts. The set of arguments may be directly
// used as parameters to contract invocation.
func GetVerifyProofArgs(proof groth16.Proof, publicWitness witness.Witness) (*VerifyProofArgs, error) {
if proof == nil {
return nil, errors.New("nil proof")
}
if proof.CurveID() != ecc.BLS12_381 {
return nil, fmt.Errorf("unexpected elliptic curve: %s", proof.CurveID())
}
// If a full witness was provided, then retrieve public part, we don't need the secret part of it.
publicWitness, err := publicWitness.Public()
if err != nil {
return nil, fmt.Errorf("failed to retrieve public witness: %w", err)
}
// Get the proof bytes (points are in the compressed form, as Verification contract accepts it).
proofSizeCompressed := int64(bls12381.SizeOfG1AffineCompressed + bls12381.SizeOfG2AffineCompressed + bls12381.SizeOfG1AffineCompressed)
var buf bytes.Buffer
n, err := proof.WriteTo(&buf)
if err != nil {
return nil, fmt.Errorf("failed to serialize proof: %w", err)
}
if n < proofSizeCompressed {
return nil, fmt.Errorf("unexpected serialized proof length: expected at least %d, got %d", proofSizeCompressed, n)
}
proofBytes := slice.Copy(buf.Bytes())
aBytes := proofBytes[:bls12381.SizeOfG1AffineCompressed]
bBytes := proofBytes[bls12381.SizeOfG1AffineCompressed : bls12381.SizeOfG1AffineCompressed+bls12381.SizeOfG2AffineCompressed]
cBytes := proofBytes[bls12381.SizeOfG1AffineCompressed+bls12381.SizeOfG2AffineCompressed : bls12381.SizeOfG1AffineCompressed+bls12381.SizeOfG2AffineCompressed+bls12381.SizeOfG1AffineCompressed]
publicWitnessBytes, err := publicWitness.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("failed to encode public witness: %w", err)
}
numPublicWitness := binary.BigEndian.Uint32(publicWitnessBytes[:4])
numSecretWitness := binary.BigEndian.Uint32(publicWitnessBytes[4:8])
numVectorElements := binary.BigEndian.Uint32(publicWitnessBytes[8:12])
// Ensure that serialization format is as expected (just in case).
if numSecretWitness != 0 {
return nil, fmt.Errorf("unexpected number of secret witnesses: %d", numSecretWitness)
}
if numPublicWitness+numSecretWitness != numVectorElements {
return nil, fmt.Errorf("unexpected number of public witness elements: %d", numVectorElements)
}
// Create public witness input.
input := make([]any, numVectorElements)
offset := 12
for i := range input { // firstly - public witnesses, after that - private ones (but they are missing from publicWitness anyway).
start := offset + i*fr.Bytes
end := start + fr.Bytes
slice.Reverse(publicWitnessBytes[start:end]) // gnark stores witnesses in the BE form, but native CryptoLib accepts LE-encoded fields elements (not a canonical form).
input[i] = publicWitnessBytes[start:end]
}
return &VerifyProofArgs{
A: aBytes,
B: bBytes,
C: cBytes,
PublicWitnesses: input,
}, nil
}