github.com/consensys/gnark-crypto@v0.14.0/internal/generator/ecc/template/tests/multiexp.go.tmpl (about) 1 {{ $G1TAffine := print (toUpper .G1.PointName) "Affine" }} 2 {{ $G1TJacobian := print (toUpper .G1.PointName) "Jac" }} 3 {{ $G1TJacobianExtended := print (toLower .G1.PointName) "JacExtended" }} 4 5 {{ $G2TAffine := print (toUpper .G2.PointName) "Affine" }} 6 {{ $G2TJacobian := print (toUpper .G2.PointName) "Jac" }} 7 {{ $G2TJacobianExtended := print (toLower .G2.PointName) "JacExtended" }} 8 9 10 import ( 11 "fmt" 12 "runtime" 13 "math/rand/v2" 14 "math/big" 15 "testing" 16 "math/bits" 17 "sync" 18 19 "github.com/consensys/gnark-crypto/ecc" 20 "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fr" 21 "github.com/leanovate/gopter" 22 "github.com/leanovate/gopter/prop" 23 ) 24 25 26 {{- if ne .Name "secp256k1"}} 27 {{template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "cmax" 16}} 28 {{template "multiexp" dict "PointName" .G2.PointName "UPointName" (toUpper .G2.PointName) "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange "cmax" 16}} 29 {{- else}} 30 {{template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "cmax" 15}} 31 {{- end}} 32 33 {{define "multiexp" }} 34 35 func TestMultiExp{{$.UPointName}}(t *testing.T) { 36 37 parameters := gopter.DefaultTestParameters() 38 if testing.Short() { 39 parameters.MinSuccessfulTests = 3 40 } else { 41 parameters.MinSuccessfulTests = nbFuzzShort * 2 42 } 43 44 45 properties := gopter.NewProperties(parameters) 46 47 genScalar := GenFr() 48 49 // size of the multiExps 50 const nbSamples = 73 51 52 // multi exp points 53 var samplePoints [nbSamples]{{ $.TAffine }} 54 var g {{ $.TJacobian }} 55 g.Set(&{{ toLower $.PointName }}Gen) 56 for i := 1; i <= nbSamples; i++ { 57 samplePoints[i-1].FromJacobian(&g) 58 g.AddAssign(&{{ toLower $.PointName }}Gen) 59 } 60 61 // sprinkle some points at infinity 62 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 63 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 64 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 65 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 66 67 // final scalar to use in double and add method (without mixer factor) 68 // n(n+1)(2n+1)/6 (sum of the squares from 1 to n) 69 var scalar big.Int 70 scalar.SetInt64(nbSamples) 71 scalar.Mul(&scalar, new(big.Int).SetInt64(nbSamples+1)) 72 scalar.Mul(&scalar, new(big.Int).SetInt64(2*nbSamples+1)) 73 scalar.Div(&scalar, new(big.Int).SetInt64(6)) 74 75 76 // ensure a multiexp that's splitted has the same result as a non-splitted one.. 77 properties.Property("[{{ $.UPointName }}] Multi exponentiation (cmax) should be consistent with splitted multiexp", prop.ForAll( 78 func(mixer fr.Element) bool { 79 var samplePointsLarge [nbSamples*13]{{ $.TAffine }} 80 for i:=0; i<13; i++ { 81 copy(samplePointsLarge[i*nbSamples:], samplePoints[:]) 82 } 83 84 var rmax, splitted1, splitted2 {{ $.TJacobian }} 85 86 // mixer ensures that all the words of a fpElement are set 87 var sampleScalars [nbSamples*13]fr.Element 88 89 for i := 1; i <= nbSamples; i++ { 90 sampleScalars[i-1].SetUint64(uint64(i)). 91 Mul(&sampleScalars[i-1], &mixer) 92 } 93 94 rmax.MultiExp(samplePointsLarge[:], sampleScalars[:], ecc.MultiExpConfig{}) 95 splitted1.MultiExp(samplePointsLarge[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: 128}) 96 splitted2.MultiExp(samplePointsLarge[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: 51}) 97 return rmax.Equal(&splitted1) && rmax.Equal(&splitted2) 98 }, 99 genScalar, 100 )) 101 102 // cRange is generated from template and contains the available parameters for the multiexp window size 103 {{- if eq $.PointName "g1" }} 104 cRange := []uint64{ 105 {{- range $c := $.CRange}}{{- if gt $c 1}}{{$c}},{{- end}}{{- end}} 106 } 107 if testing.Short() { 108 // test only "odd" and "even" (ie windows size divide word size vs not) 109 cRange = []uint64{5, 14} 110 } 111 {{- else }} 112 // for g2, CI suffers with large c size since it needs to allocate a lot of memory for the buckets. 113 // test only "odd" and "even" (ie windows size divide word size vs not) 114 cRange := []uint64{5, 14} 115 {{- end}} 116 117 properties.Property(fmt.Sprintf("[{{ $.UPointName }}] Multi exponentiation (c in %v) should be consistent with sum of square", cRange), prop.ForAll( 118 func(mixer fr.Element) bool { 119 120 var expected {{ $.TJacobian }} 121 122 // compute expected result with double and add 123 var finalScalar,mixerBigInt big.Int 124 finalScalar.Mul(&scalar, mixer.BigInt(&mixerBigInt)) 125 expected.ScalarMultiplication(&{{ toLower $.PointName }}Gen, &finalScalar) 126 127 // mixer ensures that all the words of a fpElement are set 128 var sampleScalars [nbSamples]fr.Element 129 130 for i := 1; i <= nbSamples; i++ { 131 sampleScalars[i-1].SetUint64(uint64(i)). 132 Mul(&sampleScalars[i-1], &mixer) 133 } 134 135 136 results := make([]{{ $.TJacobian }}, len(cRange)) 137 for i, c := range cRange { 138 _innerMsm{{ $.UPointName }}(&results[i], c, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 139 } 140 for i:=1; i < len(results);i++ { 141 if !results[i].Equal(&results[i-1]) { 142 t.Logf("result for c=%d != c=%d", cRange[i-1],cRange[i]) 143 return false 144 } 145 } 146 return true 147 }, 148 genScalar, 149 )) 150 151 properties.Property(fmt.Sprintf("[{{ $.UPointName }}] Multi exponentiation (c in %v) of points at infinity should output a point at infinity", cRange), prop.ForAll( 152 func(mixer fr.Element) bool { 153 154 var samplePointsZero [nbSamples]{{ $.TAffine }} 155 156 var expected {{ $.TJacobian }} 157 158 // compute expected result with double and add 159 var finalScalar, mixerBigInt big.Int 160 finalScalar.Mul(&scalar, mixer.BigInt(&mixerBigInt)) 161 expected.ScalarMultiplication(&{{ toLower $.PointName }}Gen, &finalScalar) 162 163 // mixer ensures that all the words of a fpElement are set 164 var sampleScalars [nbSamples]fr.Element 165 166 for i := 1; i <= nbSamples; i++ { 167 sampleScalars[i-1].SetUint64(uint64(i)). 168 Mul(&sampleScalars[i-1], &mixer) 169 samplePointsZero[i-1].setInfinity() 170 } 171 172 results := make([]{{ $.TJacobian }}, len(cRange)) 173 for i, c := range cRange { 174 _innerMsm{{ $.UPointName }}(&results[i], c, samplePointsZero[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 175 } 176 for i := 0; i < len(results); i++ { 177 if !results[i].Z.IsZero() { 178 t.Logf("result for c=%d is not infinity", cRange[i]) 179 return false 180 } 181 } 182 return true 183 }, 184 genScalar, 185 )) 186 187 properties.Property(fmt.Sprintf("[{{ $.UPointName }}] Multi exponentiation (c in %v) with a vector of 0s as input should output a point at infinity", cRange), prop.ForAll( 188 func(mixer fr.Element) bool { 189 // mixer ensures that all the words of a fpElement are set 190 var sampleScalars [nbSamples]fr.Element 191 192 193 results := make([]{{ $.TJacobian }}, len(cRange)) 194 for i, c := range cRange { 195 _innerMsm{{ $.UPointName }}(&results[i], c, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 196 } 197 for i := 0; i < len(results); i++ { 198 if !results[i].Z.IsZero() { 199 t.Logf("result for c=%d is not infinity", cRange[i]) 200 return false 201 } 202 } 203 return true 204 }, 205 genScalar, 206 )) 207 208 209 // note : this test is here as we expect to have a different multiExp than the above bucket method 210 // for small number of points 211 properties.Property("[{{ $.UPointName }}] Multi exponentiation (<50points) should be consistent with sum of square", prop.ForAll( 212 func(mixer fr.Element) bool { 213 214 var g {{ $.TJacobian }} 215 g.Set(&{{ toLower .PointName}}Gen) 216 217 // mixer ensures that all the words of a fpElement are set 218 samplePoints := make([]{{ $.TAffine }}, 30) 219 sampleScalars := make([]fr.Element, 30) 220 221 for i := 1; i <= 30; i++ { 222 sampleScalars[i-1].SetUint64(uint64(i)). 223 Mul(&sampleScalars[i-1], &mixer) 224 samplePoints[i-1].FromJacobian(&g) 225 g.AddAssign(&{{ toLower .PointName}}Gen) 226 } 227 228 var op1MultiExp {{ $.TAffine }} 229 op1MultiExp.MultiExp(samplePoints, sampleScalars, ecc.MultiExpConfig{}) 230 231 var finalBigScalar fr.Element 232 var finalBigScalarBi big.Int 233 var op1ScalarMul {{ $.TAffine }} 234 finalBigScalar.SetUint64(9455).Mul(&finalBigScalar, &mixer) 235 finalBigScalar.BigInt(&finalBigScalarBi) 236 op1ScalarMul.ScalarMultiplication(&{{ toLower .PointName}}GenAff, &finalBigScalarBi) 237 238 return op1ScalarMul.Equal(&op1MultiExp) 239 }, 240 genScalar, 241 )) 242 243 244 properties.TestingRun(t, gopter.ConsoleReporter(false)) 245 } 246 247 248 func TestCrossMultiExp{{ $.UPointName }}(t *testing.T) { 249 const nbSamples = 1 << 14 250 // multi exp points 251 var samplePoints [nbSamples]{{ $.TAffine }} 252 var g {{ $.TJacobian }} 253 g.Set(&{{ toLower $.PointName }}Gen) 254 for i := 1; i <= nbSamples; i++ { 255 samplePoints[i-1].FromJacobian(&g) 256 g.AddAssign(&{{ toLower $.PointName }}Gen) 257 } 258 259 // sprinkle some points at infinity 260 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 261 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 262 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 263 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 264 265 266 var sampleScalars [nbSamples]fr.Element 267 fillBenchScalars(sampleScalars[:]) 268 269 // sprinkle some doublings 270 for i:=10; i < 100; i++ { 271 samplePoints[i] = samplePoints[0] 272 sampleScalars[i] = sampleScalars[0] 273 } 274 275 // cRange is generated from template and contains the available parameters for the multiexp window size 276 {{- if eq $.PointName "g1" }} 277 cRange := []uint64{ 278 {{- range $c := $.CRange}}{{- if gt $c 1}}{{$c}},{{- end}}{{- end}} 279 } 280 if testing.Short() { 281 // test only "odd" and "even" (ie windows size divide word size vs not) 282 cRange = []uint64{5, 14} 283 } 284 {{- else }} 285 // for g2, CI suffers with large c size since it needs to allocate a lot of memory for the buckets. 286 // test only "odd" and "even" (ie windows size divide word size vs not) 287 cRange := []uint64{5, 14} 288 {{- end}} 289 290 results := make([]{{ $.TJacobian }}, len(cRange)) 291 for i, c := range cRange { 292 _innerMsm{{ $.UPointName }}(&results[i], c, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 293 } 294 295 var r {{ $.TJacobian }} 296 _innerMsm{{ $.UPointName }}Reference(&r, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 297 298 var expected, got {{ $.TAffine}} 299 expected.FromJacobian(&r) 300 301 for i:=0; i<len(results);i++ { 302 got.FromJacobian(&results[i]) 303 if !expected.Equal(&got) { 304 t.Fatalf("cross msm failed with c=%d", cRange[i]) 305 } 306 } 307 308 } 309 310 311 312 // _innerMsm{{ $.UPointName }}Reference always do ext jacobian with c == {{$.cmax}} 313 func _innerMsm{{ $.UPointName }}Reference(p *{{ $.TJacobian }}, points []{{ $.TAffine }}, scalars []fr.Element, config ecc.MultiExpConfig) *{{ $.TJacobian }} { 314 // partition the scalars 315 digits, _ := partitionScalars(scalars, {{$.cmax}}, config.NbTasks) 316 317 nbChunks := computeNbChunks({{$.cmax}}) 318 319 // for each chunk, spawn one go routine that'll loop through all the scalars in the 320 // corresponding bit-window 321 // note that buckets is an array allocated on the stack and this is critical for performance 322 323 // each go routine sends its result in chChunks[i] channel 324 chChunks := make([]chan {{ $.TJacobianExtended }}, nbChunks) 325 for i:=0; i < len(chChunks);i++ { 326 chChunks[i] = make(chan {{ $.TJacobianExtended }}, 1) 327 } 328 329 // the last chunk may be processed with a different method than the rest, as it could be smaller. 330 n := len(points) 331 for j := int(nbChunks - 1); j >= 0; j-- { 332 processChunk := processChunk{{ $.UPointName }}Jacobian[bucket{{ $.TJacobianExtended }}C{{$.cmax}}] 333 go processChunk(uint64(j), chChunks[j], {{$.cmax}}, points, digits[j*n:(j+1)*n], nil) 334 } 335 336 return msmReduceChunk{{ $.TAffine }}(p, int({{$.cmax}}), chChunks[:]) 337 } 338 339 340 func BenchmarkMultiExp{{ $.UPointName }}(b *testing.B) { 341 342 const ( 343 pow = (bits.UintSize / 2) - (bits.UintSize / 8) // 24 on 64 bits arch, 12 on 32 bits 344 nbSamples = 1 << pow 345 ) 346 347 var ( 348 samplePoints [nbSamples]{{ $.TAffine }} 349 sampleScalars [nbSamples]fr.Element 350 sampleScalarsSmallValues [nbSamples]fr.Element 351 sampleScalarsRedundant [nbSamples]fr.Element 352 ) 353 354 fillBenchScalars(sampleScalars[:]) 355 copy(sampleScalarsSmallValues[:],sampleScalars[:]) 356 copy(sampleScalarsRedundant[:],sampleScalars[:]) 357 358 // this means first chunk is going to have more work to do and should be split into several go routines 359 for i:=0; i < len(sampleScalarsSmallValues);i++ { 360 if i % 5 == 0 { 361 sampleScalarsSmallValues[i].SetZero() 362 sampleScalarsSmallValues[i][0] = 1 363 } 364 } 365 366 // bad case for batch affine because scalar distribution might look uniform 367 // but over batchSize windows, we may hit a lot of conflicts and force the msm-affine 368 // to process small batches of additions to flush its queue of conflicted points. 369 for i:=0; i < len(sampleScalarsRedundant);i+=100 { 370 for j:=i+1; j < i+100 && j < len(sampleScalarsRedundant);j++ { 371 sampleScalarsRedundant[j] = sampleScalarsRedundant[i] 372 } 373 } 374 375 fillBenchBases{{ $.UPointName }}(samplePoints[:]) 376 377 378 var testPoint {{ $.TAffine }} 379 380 for i := 5; i <= pow; i++ { 381 using := 1 << i 382 383 b.Run(fmt.Sprintf("%d points", using), func(b *testing.B) { 384 b.ResetTimer() 385 for j := 0; j < b.N; j++ { 386 testPoint.MultiExp(samplePoints[:using], sampleScalars[:using],ecc.MultiExpConfig{}) 387 } 388 }) 389 390 b.Run(fmt.Sprintf("%d points-smallvalues", using), func(b *testing.B) { 391 b.ResetTimer() 392 for j := 0; j < b.N; j++ { 393 testPoint.MultiExp(samplePoints[:using], sampleScalarsSmallValues[:using],ecc.MultiExpConfig{}) 394 } 395 }) 396 397 b.Run(fmt.Sprintf("%d points-redundancy", using), func(b *testing.B) { 398 b.ResetTimer() 399 for j := 0; j < b.N; j++ { 400 testPoint.MultiExp(samplePoints[:using], sampleScalarsRedundant[:using],ecc.MultiExpConfig{}) 401 } 402 }) 403 } 404 } 405 406 407 func BenchmarkMultiExp{{ $.UPointName }}Reference(b *testing.B) { 408 const nbSamples = 1 << 20 409 410 var ( 411 samplePoints [nbSamples]{{ $.TAffine }} 412 sampleScalars [nbSamples]fr.Element 413 ) 414 415 fillBenchScalars(sampleScalars[:]) 416 fillBenchBases{{ $.UPointName }}(samplePoints[:]) 417 418 var testPoint {{ $.TAffine }} 419 420 b.ResetTimer() 421 for j := 0; j < b.N; j++ { 422 testPoint.MultiExp(samplePoints[:], sampleScalars[:],ecc.MultiExpConfig{}) 423 } 424 } 425 426 427 func BenchmarkManyMultiExp{{ $.UPointName }}Reference(b *testing.B) { 428 const nbSamples = 1 << 20 429 430 var ( 431 samplePoints [nbSamples]{{ $.TAffine }} 432 sampleScalars [nbSamples]fr.Element 433 ) 434 435 fillBenchScalars(sampleScalars[:]) 436 fillBenchBases{{ $.UPointName }}(samplePoints[:]) 437 438 439 var t1, t2, t3 {{ $.TAffine }} 440 b.ResetTimer() 441 for j := 0; j < b.N; j++ { 442 var wg sync.WaitGroup 443 wg.Add(3) 444 go func() { 445 t1.MultiExp(samplePoints[:], sampleScalars[:],ecc.MultiExpConfig{}) 446 wg.Done() 447 }() 448 go func() { 449 t2.MultiExp(samplePoints[:], sampleScalars[:],ecc.MultiExpConfig{}) 450 wg.Done() 451 }() 452 go func() { 453 t3.MultiExp(samplePoints[:], sampleScalars[:],ecc.MultiExpConfig{}) 454 wg.Done() 455 }() 456 wg.Wait() 457 } 458 } 459 460 // WARNING: this return points that are NOT on the curve and is meant to be use for benchmarking 461 // purposes only. We don't check that the result is valid but just measure "computational complexity". 462 // 463 // Rationale for generating points that are not on the curve is that for large benchmarks, generating 464 // a vector of different points can take minutes. Using the same point or subset will bias the benchmark result 465 // since bucket additions in extended jacobian coordinates will hit doubling algorithm instead of add. 466 func fillBenchBases{{ $.UPointName }}(samplePoints []{{ $.TAffine }}) { 467 var r big.Int 468 r.SetString("340444420969191673093399857471996460938405", 10) 469 samplePoints[0].ScalarMultiplication(&samplePoints[0], &r) 470 471 one := samplePoints[0].X 472 one.SetOne() 473 474 for i := 1; i < len(samplePoints); i++ { 475 samplePoints[i].X.Add(&samplePoints[i-1].X, &one) 476 samplePoints[i].Y.Sub(&samplePoints[i-1].Y, &one) 477 } 478 } 479 480 481 {{end }} 482 483 484 func fillBenchScalars(sampleScalars []fr.Element) { 485 // ensure every words of the scalars are filled 486 for i := 0; i < len(sampleScalars); i++ { 487 sampleScalars[i].SetRandom() 488 } 489 }