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 }