github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/smartcontract/zkpbinding/binding.go (about)

     1  // Package zkpbinding contains a set of helper functions aimed to generate and
     2  // interact with Verifier smart contract written in Go and using Groth-16 proving
     3  // system over BLS12-381 elliptic curve to verify proofs. Package zkpbinding
     4  // provides the Veifier contract generation functionality itself as far as a
     5  // helper that converts groth16.Proof to the Verifier-specific set of arguments.
     6  //
     7  // Please, check out the example of zkpbinding package usage to generate and
     8  // verify proofs on the Neo chain:
     9  // https://github.com/nspcc-dev/neo-go/blob/91c928e8d35164055e5b2e8efbc898440cc2b486/examples/zkp/cubic_circuit/README.md
    10  package zkpbinding
    11  
    12  import (
    13  	"encoding/binary"
    14  	"errors"
    15  	"fmt"
    16  	"io"
    17  	"text/template"
    18  
    19  	"github.com/consensys/gnark-crypto/ecc"
    20  	"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
    21  	"github.com/consensys/gnark/backend/groth16"
    22  	curve "github.com/consensys/gnark/backend/groth16/bls12-381"
    23  	"github.com/consensys/gnark/backend/witness"
    24  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/binding"
    25  	"github.com/nspcc-dev/neo-go/pkg/util/slice"
    26  )
    27  
    28  // Config represents a configuration for Verifier Go smart contract generator.
    29  type Config struct {
    30  	// VerifyingKey must be a Groth-16 BLS12-381 specific verifier key,
    31  	// parameters of which will be used to generate Verifier Neo smart contract.
    32  	VerifyingKey groth16.VerifyingKey
    33  	// Output is a writer for the resulting Verifier Go smart contract, it must
    34  	// not be nil.
    35  	Output io.Writer
    36  	// CfgOutput is a writer for the resulting Verifier Go smart contract YAML
    37  	// configuration file needed to compile the contract. It may be nil if the
    38  	// contract configuration file generation should be omitted.
    39  	CfgOutput io.Writer
    40  	// GomodOutput is a writer for the resulting go.mod file of the Verifier Go
    41  	// smart contract needed to compile it. It may be nil if the go.mod file
    42  	// generation should be omitted.
    43  	GomodOutput io.Writer
    44  	// GosumOutput is a writer for the resulting go.sum file of the Verifier Go
    45  	// smart contract needed to compile it. It may be nil if the go.sum file
    46  	// generation should be omitted.
    47  	GosumOutput io.Writer
    48  }
    49  
    50  // A set of Verifier smart contract template related constants.
    51  const (
    52  	// goVerificationTmpl is a verification smart contract template. It contains
    53  	// a single `verifyProof` method that accepts a proof represented as three
    54  	// BLS12-381 curve points and public information required for verification
    55  	// represented as a list of serialized 32-bytes field elements in the LE form.
    56  	// The boolean result of `verifyProof` is either `true` (if the proof is
    57  	// valid) or `false` (if the proof is invalid). The smart contract generated
    58  	// from this template can be immediately compiled without any additional
    59  	// changes using NeoGo compiler, deployed to the Neo chain and invoked. The
    60  	// verification contract is circuit-specific, i.e. corresponds to a specific
    61  	// single constraint system. Thus, every new circuit requires vew verification
    62  	// contract to be generated and deployed to the chain.
    63  	goVerificationTmpl = `//Code generated by neo-go zkpbinding.GenerateVerifier; DO NOT EDIT.
    64  
    65  // Package main contains verification smart contract that uses Neo BLS12-381
    66  // curves interoperability functionality to verify provided proof against provided
    67  // public input. The contract contains a single 'verifyProof' method that accepts
    68  // a proof represented as three BLS12-381 curve points and public witnesses
    69  // required for verification represented as a list of serialized 32-bytes field
    70  // elements in the LE form. This contract is circuit-specific and can not be used
    71  // to verify other circuits.
    72  //
    73  // Use NeoGo smart contract compiler to compile this contract:
    74  // https://github.com/nspcc-dev/neo-go/blob/master/docs/compiler.md#compiling.
    75  // You will need to create contract YAML configuration file and proper go.mod and
    76  // go.sum files required for compilation. Please, refer to the NeoGo ZKP example
    77  // to see how to verify proofs via the Verifier contract:
    78  // https://github.com/nspcc-dev/neo-go/tree/master/examples/zkp/cubic_circuit.
    79  package main
    80  
    81  import (
    82  	"github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
    83  	"github.com/nspcc-dev/neo-go/pkg/interop/util"
    84  )
    85  
    86  // A set of circuit-specific variables required for verification. Should be generated
    87  // using MPC process.
    88  var (
    89  	// G1 Affine point.
    90  	alpha = []byte{{ byteSliceToStr .Alpha }}
    91  	// G2 Affine point.
    92  	beta = []byte{{ byteSliceToStr .Beta }}
    93  	// G2 Affine point.
    94  	gamma = []byte{{ byteSliceToStr .Gamma }}
    95  	// G2 Affine point.
    96  	delta = []byte{{ byteSliceToStr .Delta }}
    97  	// A set of G1 Affine points.
    98  	ic = [][]byte{
    99  		{{- range $i := .ICs }}
   100  		{{ byteSliceToStr $i }},{{ end -}}
   101  	}
   102  )
   103  
   104  // VerifyProof verifies the given proof represented as three serialized compressed
   105  // BLS12-381 points against the public information represented as a list of
   106  // serialized 32-bytes field elements in the LE form. Verification process
   107  // follows the Groth-16 proving system and is taken from the
   108  // https://github.com/neo-project/neo/issues/2647#issuecomment-1002893109 without
   109  // any changes. Verification process checks the following equality:
   110  //
   111  //	A * B = alpha * beta + sum(pub_input[i] * (beta * u_i(x) + alpha * v_i(x) + w_i(x)) / gamma) * gamma + C * delta
   112  func VerifyProof(a []byte, b []byte, c []byte, publicInput [][]byte) bool {
   113  	alphaPoint := crypto.Bls12381Deserialize(alpha)
   114  	betaPoint := crypto.Bls12381Deserialize(beta)
   115  	gammaPoint := crypto.Bls12381Deserialize(gamma)
   116  	deltaPoint := crypto.Bls12381Deserialize(delta)
   117  
   118  	aPoint := crypto.Bls12381Deserialize(a)
   119  	bPoint := crypto.Bls12381Deserialize(b)
   120  	cPoint := crypto.Bls12381Deserialize(c)
   121  
   122  	// Equation left1: A*B
   123  	lt := crypto.Bls12381Pairing(aPoint, bPoint)
   124  
   125  	// Equation right1: alpha*beta
   126  	rt1 := crypto.Bls12381Pairing(alphaPoint, betaPoint)
   127  
   128  	// Equation right2: sum(pub_input[i]*(beta*u_i(x)+alpha*v_i(x)+w_i(x))/gamma)*gamma
   129  	inputlen := len(publicInput)
   130  	iclen := len(ic)
   131  
   132  	if iclen != inputlen+1 {
   133  		panic("error: inputlen or iclen")
   134  	}
   135  	icPoints := make([]crypto.Bls12381Point, iclen)
   136  	for i := 0; i < iclen; i++ {
   137  		icPoints[i] = crypto.Bls12381Deserialize(ic[i])
   138  	}
   139  	acc := icPoints[0]
   140  	for i := 0; i < inputlen; i++ {
   141  		scalar := publicInput[i] // 32-bytes LE field element.
   142  		temp := crypto.Bls12381Mul(icPoints[i+1], scalar, false)
   143  		acc = crypto.Bls12381Add(acc, temp)
   144  	}
   145  	rt2 := crypto.Bls12381Pairing(acc, gammaPoint)
   146  
   147  	// Equation right3: C*delta
   148  	rt3 := crypto.Bls12381Pairing(cPoint, deltaPoint)
   149  
   150  	// Check equality.
   151  	t1 := crypto.Bls12381Add(rt1, rt2)
   152  	t2 := crypto.Bls12381Add(t1, rt3)
   153  
   154  	return util.Equals(lt, t2)
   155  }
   156  `
   157  
   158  	// verifyCfg is a contract configuration file required to compile smart
   159  	// contract.
   160  	verifyCfg = `name: "Groth-16 Verifier contract"
   161  sourceurl: https://github.com/nspcc-dev/neo-go/
   162  supportedstandards: []`
   163  
   164  	// verifyGomod is a standard go.mod file containing module name, go version
   165  	// and dependency packages version needed for smart contract compilation.
   166  	verifyGomod = `module verify
   167  
   168  go 1.20
   169  
   170  require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231004150345-8849ccde2524
   171  `
   172  
   173  	// verifyGosum is a standard go.sum file needed for contract compilation.
   174  	verifyGosum = `github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231004150345-8849ccde2524 h1:LKp/89ftf+MwMExKgnbwjQp5zQTUZ3lDCc+DZ4VeSRc=
   175  github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231004150345-8849ccde2524/go.mod h1:ZUuXOkdtHZgaC13za/zMgXfQFncZ0jLzfQTe+OsDOtg=
   176  `
   177  )
   178  
   179  // VerifyProofArgs is the set of arguments of `verifyProof` method of a
   180  // Verifier contract in serialized form (as the contract accepts them).
   181  type VerifyProofArgs struct {
   182  	A               []byte
   183  	B               []byte
   184  	C               []byte
   185  	PublicWitnesses []any
   186  }
   187  
   188  // tmplParams is a set of parameters used by verification contract template.
   189  type tmplParams struct {
   190  	Alpha []byte
   191  	Beta  []byte
   192  	Gamma []byte
   193  	Delta []byte
   194  	ICs   [][]byte
   195  }
   196  
   197  // GenerateVerifier generates a Verifier smart contract written in Go for Neo
   198  // blockchain. The contract contains a single `verifyProof` method that accepts
   199  // a proof represented as three BLS12-381 curve points and public witnesses
   200  // required for verification represented as a list of serialized 32-bytes field
   201  // elements in the LE form. The boolean result of `verifyProof` is either `true`
   202  // (if the proof is valid) or `false` (if the proof is invalid). The smart
   203  // contract generated from this template can be immediately compiled without
   204  // any additional changes using NeoGo compiler, deployed to the Neo chain and
   205  // invoked. The verification contract is circuit-specific, i.e. corresponds to
   206  // a specific constraint system. Thus, every new circuit requires its own
   207  // verification contract to be generated and deployed to the chain.
   208  //
   209  // GenerateVerifier also generates a proper contract YAML configuration file,
   210  // go.mod and go.sum files if the corresponding writers are provided via cfg.
   211  func GenerateVerifier(cfg Config) error {
   212  	if cfg.VerifyingKey == nil {
   213  		return fmt.Errorf("nil verifying key")
   214  	}
   215  	if cfg.VerifyingKey.CurveID() != ecc.BLS12_381 {
   216  		return fmt.Errorf("unexpected elliptic curve: %s", cfg.VerifyingKey.CurveID())
   217  	}
   218  
   219  	// Fetch the contract's public verification parameters. We can directly access
   220  	// the VerifyingKey elements since gnark v0.9.0.
   221  	vk := cfg.VerifyingKey.(*curve.VerifyingKey)
   222  	alphaG1 := vk.G1.Alpha.Bytes()
   223  	betaG2 := vk.G2.Beta.Bytes()
   224  	gammaG2 := vk.G2.Gamma.Bytes()
   225  	deltaG2 := vk.G2.Delta.Bytes()
   226  	kvks := make([][]byte, len(vk.G1.K))
   227  	for i := range kvks {
   228  		arr := vk.G1.K[i].Bytes()
   229  		kvks[i] = arr[:]
   230  	}
   231  
   232  	// Generate verification contract from the template using the retrieved
   233  	// verification parameters.
   234  	tmpl := template.Must(template.New("generate").Funcs(template.FuncMap{
   235  		"byteSliceToStr": byteSliceToStr,
   236  	}).Parse(goVerificationTmpl))
   237  
   238  	err := binding.FExecute(tmpl, cfg.Output, tmplParams{
   239  		Alpha: alphaG1[:],
   240  		Beta:  betaG2[:],
   241  		Gamma: gammaG2[:],
   242  		Delta: deltaG2[:],
   243  		ICs:   kvks,
   244  	})
   245  	if err != nil {
   246  		return err
   247  	}
   248  
   249  	if cfg.CfgOutput != nil {
   250  		_, err = cfg.CfgOutput.Write([]byte(verifyCfg))
   251  		if err != nil {
   252  			return fmt.Errorf("failed to generate contract configuration file: %w", err)
   253  		}
   254  	}
   255  	if cfg.GomodOutput != nil {
   256  		_, err = cfg.GomodOutput.Write([]byte(verifyGomod))
   257  		if err != nil {
   258  			return fmt.Errorf("failed to generate go.mod file: %w", err)
   259  		}
   260  	}
   261  	if cfg.GosumOutput != nil {
   262  		_, err = cfg.GosumOutput.Write([]byte(verifyGosum))
   263  		if err != nil {
   264  			return fmt.Errorf("failed to generate go.mod file: %w", err)
   265  		}
   266  	}
   267  
   268  	return nil
   269  }
   270  
   271  // byteSliceToStr is a codegen helper that converts byte slice to a go-like slice.
   272  func byteSliceToStr(s []byte) string {
   273  	var res string
   274  	for _, b := range s {
   275  		res += fmt.Sprintf("%d, ", b)
   276  	}
   277  	return `{` + res[:len(res)-2] + `}`
   278  }
   279  
   280  // GetVerifyProofArgs returns a serialized set of arguments `verifyProof` method
   281  // of a generated Verifier contract accepts. The set of arguments may be directly
   282  // used as parameters to contract invocation.
   283  func GetVerifyProofArgs(proof groth16.Proof, publicWitness witness.Witness) (*VerifyProofArgs, error) {
   284  	if proof == nil {
   285  		return nil, errors.New("nil proof")
   286  	}
   287  	if proof.CurveID() != ecc.BLS12_381 {
   288  		return nil, fmt.Errorf("unexpected elliptic curve: %s", proof.CurveID())
   289  	}
   290  	// If a full witness was provided, then retrieve public part, we don't need the secret part of it.
   291  	publicWitness, err := publicWitness.Public()
   292  	if err != nil {
   293  		return nil, fmt.Errorf("failed to retrieve public witness: %w", err)
   294  	}
   295  	// Get the proof bytes (points are in the compressed form, as Verification contract accepts it).
   296  	p := proof.(*curve.Proof)
   297  	aBytes := p.Ar.Bytes()
   298  	bBytes := p.Bs.Bytes()
   299  	cBytes := p.Krs.Bytes()
   300  
   301  	publicWitnessBytes, err := publicWitness.MarshalBinary()
   302  	if err != nil {
   303  		return nil, fmt.Errorf("failed to encode public witness: %w", err)
   304  	}
   305  	numPublicWitness := binary.BigEndian.Uint32(publicWitnessBytes[:4])
   306  	numSecretWitness := binary.BigEndian.Uint32(publicWitnessBytes[4:8])
   307  	numVectorElements := binary.BigEndian.Uint32(publicWitnessBytes[8:12])
   308  
   309  	// Ensure that serialization format is as expected (just in case).
   310  	if numSecretWitness != 0 {
   311  		return nil, fmt.Errorf("unexpected number of secret witnesses: %d", numSecretWitness)
   312  	}
   313  	if numPublicWitness+numSecretWitness != numVectorElements {
   314  		return nil, fmt.Errorf("unexpected number of public witness elements: %d", numVectorElements)
   315  	}
   316  
   317  	// Create public witness input.
   318  	input := make([]any, numVectorElements)
   319  	offset := 12
   320  	for i := range input { // firstly - public witnesses, after that - private ones (but they are missing from publicWitness anyway).
   321  		start := offset + i*fr.Bytes
   322  		end := start + fr.Bytes
   323  		slice.Reverse(publicWitnessBytes[start:end]) // gnark stores witnesses in the BE form, but native CryptoLib accepts LE-encoded fields elements (not a canonical form).
   324  		input[i] = publicWitnessBytes[start:end]
   325  	}
   326  	return &VerifyProofArgs{
   327  		A:               aBytes[:],
   328  		B:               bBytes[:],
   329  		C:               cBytes[:],
   330  		PublicWitnesses: input,
   331  	}, nil
   332  }