github.com/consensys/gnark-crypto@v0.14.0/ecc/bn254/multiexp_test.go (about) 1 // Copyright 2020 Consensys Software Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Code generated by consensys/gnark-crypto DO NOT EDIT 16 17 package bn254 18 19 import ( 20 "fmt" 21 "math/big" 22 "math/bits" 23 "math/rand/v2" 24 "runtime" 25 "sync" 26 "testing" 27 28 "github.com/consensys/gnark-crypto/ecc" 29 "github.com/consensys/gnark-crypto/ecc/bn254/fr" 30 "github.com/leanovate/gopter" 31 "github.com/leanovate/gopter/prop" 32 ) 33 34 func TestMultiExpG1(t *testing.T) { 35 36 parameters := gopter.DefaultTestParameters() 37 if testing.Short() { 38 parameters.MinSuccessfulTests = 3 39 } else { 40 parameters.MinSuccessfulTests = nbFuzzShort * 2 41 } 42 43 properties := gopter.NewProperties(parameters) 44 45 genScalar := GenFr() 46 47 // size of the multiExps 48 const nbSamples = 73 49 50 // multi exp points 51 var samplePoints [nbSamples]G1Affine 52 var g G1Jac 53 g.Set(&g1Gen) 54 for i := 1; i <= nbSamples; i++ { 55 samplePoints[i-1].FromJacobian(&g) 56 g.AddAssign(&g1Gen) 57 } 58 59 // sprinkle some points at infinity 60 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 61 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 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 65 // final scalar to use in double and add method (without mixer factor) 66 // n(n+1)(2n+1)/6 (sum of the squares from 1 to n) 67 var scalar big.Int 68 scalar.SetInt64(nbSamples) 69 scalar.Mul(&scalar, new(big.Int).SetInt64(nbSamples+1)) 70 scalar.Mul(&scalar, new(big.Int).SetInt64(2*nbSamples+1)) 71 scalar.Div(&scalar, new(big.Int).SetInt64(6)) 72 73 // ensure a multiexp that's splitted has the same result as a non-splitted one.. 74 properties.Property("[G1] Multi exponentiation (cmax) should be consistent with splitted multiexp", prop.ForAll( 75 func(mixer fr.Element) bool { 76 var samplePointsLarge [nbSamples * 13]G1Affine 77 for i := 0; i < 13; i++ { 78 copy(samplePointsLarge[i*nbSamples:], samplePoints[:]) 79 } 80 81 var rmax, splitted1, splitted2 G1Jac 82 83 // mixer ensures that all the words of a fpElement are set 84 var sampleScalars [nbSamples * 13]fr.Element 85 86 for i := 1; i <= nbSamples; i++ { 87 sampleScalars[i-1].SetUint64(uint64(i)). 88 Mul(&sampleScalars[i-1], &mixer) 89 } 90 91 rmax.MultiExp(samplePointsLarge[:], sampleScalars[:], ecc.MultiExpConfig{}) 92 splitted1.MultiExp(samplePointsLarge[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: 128}) 93 splitted2.MultiExp(samplePointsLarge[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: 51}) 94 return rmax.Equal(&splitted1) && rmax.Equal(&splitted2) 95 }, 96 genScalar, 97 )) 98 99 // cRange is generated from template and contains the available parameters for the multiexp window size 100 cRange := []uint64{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} 101 if testing.Short() { 102 // test only "odd" and "even" (ie windows size divide word size vs not) 103 cRange = []uint64{5, 14} 104 } 105 106 properties.Property(fmt.Sprintf("[G1] Multi exponentiation (c in %v) should be consistent with sum of square", cRange), prop.ForAll( 107 func(mixer fr.Element) bool { 108 109 var expected G1Jac 110 111 // compute expected result with double and add 112 var finalScalar, mixerBigInt big.Int 113 finalScalar.Mul(&scalar, mixer.BigInt(&mixerBigInt)) 114 expected.ScalarMultiplication(&g1Gen, &finalScalar) 115 116 // mixer ensures that all the words of a fpElement are set 117 var sampleScalars [nbSamples]fr.Element 118 119 for i := 1; i <= nbSamples; i++ { 120 sampleScalars[i-1].SetUint64(uint64(i)). 121 Mul(&sampleScalars[i-1], &mixer) 122 } 123 124 results := make([]G1Jac, len(cRange)) 125 for i, c := range cRange { 126 _innerMsmG1(&results[i], c, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 127 } 128 for i := 1; i < len(results); i++ { 129 if !results[i].Equal(&results[i-1]) { 130 t.Logf("result for c=%d != c=%d", cRange[i-1], cRange[i]) 131 return false 132 } 133 } 134 return true 135 }, 136 genScalar, 137 )) 138 139 properties.Property(fmt.Sprintf("[G1] Multi exponentiation (c in %v) of points at infinity should output a point at infinity", cRange), prop.ForAll( 140 func(mixer fr.Element) bool { 141 142 var samplePointsZero [nbSamples]G1Affine 143 144 var expected G1Jac 145 146 // compute expected result with double and add 147 var finalScalar, mixerBigInt big.Int 148 finalScalar.Mul(&scalar, mixer.BigInt(&mixerBigInt)) 149 expected.ScalarMultiplication(&g1Gen, &finalScalar) 150 151 // mixer ensures that all the words of a fpElement are set 152 var sampleScalars [nbSamples]fr.Element 153 154 for i := 1; i <= nbSamples; i++ { 155 sampleScalars[i-1].SetUint64(uint64(i)). 156 Mul(&sampleScalars[i-1], &mixer) 157 samplePointsZero[i-1].setInfinity() 158 } 159 160 results := make([]G1Jac, len(cRange)) 161 for i, c := range cRange { 162 _innerMsmG1(&results[i], c, samplePointsZero[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 163 } 164 for i := 0; i < len(results); i++ { 165 if !results[i].Z.IsZero() { 166 t.Logf("result for c=%d is not infinity", cRange[i]) 167 return false 168 } 169 } 170 return true 171 }, 172 genScalar, 173 )) 174 175 properties.Property(fmt.Sprintf("[G1] Multi exponentiation (c in %v) with a vector of 0s as input should output a point at infinity", cRange), prop.ForAll( 176 func(mixer fr.Element) bool { 177 // mixer ensures that all the words of a fpElement are set 178 var sampleScalars [nbSamples]fr.Element 179 180 results := make([]G1Jac, len(cRange)) 181 for i, c := range cRange { 182 _innerMsmG1(&results[i], c, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 183 } 184 for i := 0; i < len(results); i++ { 185 if !results[i].Z.IsZero() { 186 t.Logf("result for c=%d is not infinity", cRange[i]) 187 return false 188 } 189 } 190 return true 191 }, 192 genScalar, 193 )) 194 195 // note : this test is here as we expect to have a different multiExp than the above bucket method 196 // for small number of points 197 properties.Property("[G1] Multi exponentiation (<50points) should be consistent with sum of square", prop.ForAll( 198 func(mixer fr.Element) bool { 199 200 var g G1Jac 201 g.Set(&g1Gen) 202 203 // mixer ensures that all the words of a fpElement are set 204 samplePoints := make([]G1Affine, 30) 205 sampleScalars := make([]fr.Element, 30) 206 207 for i := 1; i <= 30; i++ { 208 sampleScalars[i-1].SetUint64(uint64(i)). 209 Mul(&sampleScalars[i-1], &mixer) 210 samplePoints[i-1].FromJacobian(&g) 211 g.AddAssign(&g1Gen) 212 } 213 214 var op1MultiExp G1Affine 215 op1MultiExp.MultiExp(samplePoints, sampleScalars, ecc.MultiExpConfig{}) 216 217 var finalBigScalar fr.Element 218 var finalBigScalarBi big.Int 219 var op1ScalarMul G1Affine 220 finalBigScalar.SetUint64(9455).Mul(&finalBigScalar, &mixer) 221 finalBigScalar.BigInt(&finalBigScalarBi) 222 op1ScalarMul.ScalarMultiplication(&g1GenAff, &finalBigScalarBi) 223 224 return op1ScalarMul.Equal(&op1MultiExp) 225 }, 226 genScalar, 227 )) 228 229 properties.TestingRun(t, gopter.ConsoleReporter(false)) 230 } 231 232 func TestCrossMultiExpG1(t *testing.T) { 233 const nbSamples = 1 << 14 234 // multi exp points 235 var samplePoints [nbSamples]G1Affine 236 var g G1Jac 237 g.Set(&g1Gen) 238 for i := 1; i <= nbSamples; i++ { 239 samplePoints[i-1].FromJacobian(&g) 240 g.AddAssign(&g1Gen) 241 } 242 243 // sprinkle some points at infinity 244 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 245 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 246 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 247 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 248 249 var sampleScalars [nbSamples]fr.Element 250 fillBenchScalars(sampleScalars[:]) 251 252 // sprinkle some doublings 253 for i := 10; i < 100; i++ { 254 samplePoints[i] = samplePoints[0] 255 sampleScalars[i] = sampleScalars[0] 256 } 257 258 // cRange is generated from template and contains the available parameters for the multiexp window size 259 cRange := []uint64{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} 260 if testing.Short() { 261 // test only "odd" and "even" (ie windows size divide word size vs not) 262 cRange = []uint64{5, 14} 263 } 264 265 results := make([]G1Jac, len(cRange)) 266 for i, c := range cRange { 267 _innerMsmG1(&results[i], c, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 268 } 269 270 var r G1Jac 271 _innerMsmG1Reference(&r, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 272 273 var expected, got G1Affine 274 expected.FromJacobian(&r) 275 276 for i := 0; i < len(results); i++ { 277 got.FromJacobian(&results[i]) 278 if !expected.Equal(&got) { 279 t.Fatalf("cross msm failed with c=%d", cRange[i]) 280 } 281 } 282 283 } 284 285 // _innerMsmG1Reference always do ext jacobian with c == 16 286 func _innerMsmG1Reference(p *G1Jac, points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) *G1Jac { 287 // partition the scalars 288 digits, _ := partitionScalars(scalars, 16, config.NbTasks) 289 290 nbChunks := computeNbChunks(16) 291 292 // for each chunk, spawn one go routine that'll loop through all the scalars in the 293 // corresponding bit-window 294 // note that buckets is an array allocated on the stack and this is critical for performance 295 296 // each go routine sends its result in chChunks[i] channel 297 chChunks := make([]chan g1JacExtended, nbChunks) 298 for i := 0; i < len(chChunks); i++ { 299 chChunks[i] = make(chan g1JacExtended, 1) 300 } 301 302 // the last chunk may be processed with a different method than the rest, as it could be smaller. 303 n := len(points) 304 for j := int(nbChunks - 1); j >= 0; j-- { 305 processChunk := processChunkG1Jacobian[bucketg1JacExtendedC16] 306 go processChunk(uint64(j), chChunks[j], 16, points, digits[j*n:(j+1)*n], nil) 307 } 308 309 return msmReduceChunkG1Affine(p, int(16), chChunks[:]) 310 } 311 312 func BenchmarkMultiExpG1(b *testing.B) { 313 314 const ( 315 pow = (bits.UintSize / 2) - (bits.UintSize / 8) // 24 on 64 bits arch, 12 on 32 bits 316 nbSamples = 1 << pow 317 ) 318 319 var ( 320 samplePoints [nbSamples]G1Affine 321 sampleScalars [nbSamples]fr.Element 322 sampleScalarsSmallValues [nbSamples]fr.Element 323 sampleScalarsRedundant [nbSamples]fr.Element 324 ) 325 326 fillBenchScalars(sampleScalars[:]) 327 copy(sampleScalarsSmallValues[:], sampleScalars[:]) 328 copy(sampleScalarsRedundant[:], sampleScalars[:]) 329 330 // this means first chunk is going to have more work to do and should be split into several go routines 331 for i := 0; i < len(sampleScalarsSmallValues); i++ { 332 if i%5 == 0 { 333 sampleScalarsSmallValues[i].SetZero() 334 sampleScalarsSmallValues[i][0] = 1 335 } 336 } 337 338 // bad case for batch affine because scalar distribution might look uniform 339 // but over batchSize windows, we may hit a lot of conflicts and force the msm-affine 340 // to process small batches of additions to flush its queue of conflicted points. 341 for i := 0; i < len(sampleScalarsRedundant); i += 100 { 342 for j := i + 1; j < i+100 && j < len(sampleScalarsRedundant); j++ { 343 sampleScalarsRedundant[j] = sampleScalarsRedundant[i] 344 } 345 } 346 347 fillBenchBasesG1(samplePoints[:]) 348 349 var testPoint G1Affine 350 351 for i := 5; i <= pow; i++ { 352 using := 1 << i 353 354 b.Run(fmt.Sprintf("%d points", using), func(b *testing.B) { 355 b.ResetTimer() 356 for j := 0; j < b.N; j++ { 357 testPoint.MultiExp(samplePoints[:using], sampleScalars[:using], ecc.MultiExpConfig{}) 358 } 359 }) 360 361 b.Run(fmt.Sprintf("%d points-smallvalues", using), func(b *testing.B) { 362 b.ResetTimer() 363 for j := 0; j < b.N; j++ { 364 testPoint.MultiExp(samplePoints[:using], sampleScalarsSmallValues[:using], ecc.MultiExpConfig{}) 365 } 366 }) 367 368 b.Run(fmt.Sprintf("%d points-redundancy", using), func(b *testing.B) { 369 b.ResetTimer() 370 for j := 0; j < b.N; j++ { 371 testPoint.MultiExp(samplePoints[:using], sampleScalarsRedundant[:using], ecc.MultiExpConfig{}) 372 } 373 }) 374 } 375 } 376 377 func BenchmarkMultiExpG1Reference(b *testing.B) { 378 const nbSamples = 1 << 20 379 380 var ( 381 samplePoints [nbSamples]G1Affine 382 sampleScalars [nbSamples]fr.Element 383 ) 384 385 fillBenchScalars(sampleScalars[:]) 386 fillBenchBasesG1(samplePoints[:]) 387 388 var testPoint G1Affine 389 390 b.ResetTimer() 391 for j := 0; j < b.N; j++ { 392 testPoint.MultiExp(samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{}) 393 } 394 } 395 396 func BenchmarkManyMultiExpG1Reference(b *testing.B) { 397 const nbSamples = 1 << 20 398 399 var ( 400 samplePoints [nbSamples]G1Affine 401 sampleScalars [nbSamples]fr.Element 402 ) 403 404 fillBenchScalars(sampleScalars[:]) 405 fillBenchBasesG1(samplePoints[:]) 406 407 var t1, t2, t3 G1Affine 408 b.ResetTimer() 409 for j := 0; j < b.N; j++ { 410 var wg sync.WaitGroup 411 wg.Add(3) 412 go func() { 413 t1.MultiExp(samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{}) 414 wg.Done() 415 }() 416 go func() { 417 t2.MultiExp(samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{}) 418 wg.Done() 419 }() 420 go func() { 421 t3.MultiExp(samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{}) 422 wg.Done() 423 }() 424 wg.Wait() 425 } 426 } 427 428 // WARNING: this return points that are NOT on the curve and is meant to be use for benchmarking 429 // purposes only. We don't check that the result is valid but just measure "computational complexity". 430 // 431 // Rationale for generating points that are not on the curve is that for large benchmarks, generating 432 // a vector of different points can take minutes. Using the same point or subset will bias the benchmark result 433 // since bucket additions in extended jacobian coordinates will hit doubling algorithm instead of add. 434 func fillBenchBasesG1(samplePoints []G1Affine) { 435 var r big.Int 436 r.SetString("340444420969191673093399857471996460938405", 10) 437 samplePoints[0].ScalarMultiplication(&samplePoints[0], &r) 438 439 one := samplePoints[0].X 440 one.SetOne() 441 442 for i := 1; i < len(samplePoints); i++ { 443 samplePoints[i].X.Add(&samplePoints[i-1].X, &one) 444 samplePoints[i].Y.Sub(&samplePoints[i-1].Y, &one) 445 } 446 } 447 448 func TestMultiExpG2(t *testing.T) { 449 450 parameters := gopter.DefaultTestParameters() 451 if testing.Short() { 452 parameters.MinSuccessfulTests = 3 453 } else { 454 parameters.MinSuccessfulTests = nbFuzzShort * 2 455 } 456 457 properties := gopter.NewProperties(parameters) 458 459 genScalar := GenFr() 460 461 // size of the multiExps 462 const nbSamples = 73 463 464 // multi exp points 465 var samplePoints [nbSamples]G2Affine 466 var g G2Jac 467 g.Set(&g2Gen) 468 for i := 1; i <= nbSamples; i++ { 469 samplePoints[i-1].FromJacobian(&g) 470 g.AddAssign(&g2Gen) 471 } 472 473 // sprinkle some points at infinity 474 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 475 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 476 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 477 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 478 479 // final scalar to use in double and add method (without mixer factor) 480 // n(n+1)(2n+1)/6 (sum of the squares from 1 to n) 481 var scalar big.Int 482 scalar.SetInt64(nbSamples) 483 scalar.Mul(&scalar, new(big.Int).SetInt64(nbSamples+1)) 484 scalar.Mul(&scalar, new(big.Int).SetInt64(2*nbSamples+1)) 485 scalar.Div(&scalar, new(big.Int).SetInt64(6)) 486 487 // ensure a multiexp that's splitted has the same result as a non-splitted one.. 488 properties.Property("[G2] Multi exponentiation (cmax) should be consistent with splitted multiexp", prop.ForAll( 489 func(mixer fr.Element) bool { 490 var samplePointsLarge [nbSamples * 13]G2Affine 491 for i := 0; i < 13; i++ { 492 copy(samplePointsLarge[i*nbSamples:], samplePoints[:]) 493 } 494 495 var rmax, splitted1, splitted2 G2Jac 496 497 // mixer ensures that all the words of a fpElement are set 498 var sampleScalars [nbSamples * 13]fr.Element 499 500 for i := 1; i <= nbSamples; i++ { 501 sampleScalars[i-1].SetUint64(uint64(i)). 502 Mul(&sampleScalars[i-1], &mixer) 503 } 504 505 rmax.MultiExp(samplePointsLarge[:], sampleScalars[:], ecc.MultiExpConfig{}) 506 splitted1.MultiExp(samplePointsLarge[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: 128}) 507 splitted2.MultiExp(samplePointsLarge[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: 51}) 508 return rmax.Equal(&splitted1) && rmax.Equal(&splitted2) 509 }, 510 genScalar, 511 )) 512 513 // cRange is generated from template and contains the available parameters for the multiexp window size 514 // for g2, CI suffers with large c size since it needs to allocate a lot of memory for the buckets. 515 // test only "odd" and "even" (ie windows size divide word size vs not) 516 cRange := []uint64{5, 14} 517 518 properties.Property(fmt.Sprintf("[G2] Multi exponentiation (c in %v) should be consistent with sum of square", cRange), prop.ForAll( 519 func(mixer fr.Element) bool { 520 521 var expected G2Jac 522 523 // compute expected result with double and add 524 var finalScalar, mixerBigInt big.Int 525 finalScalar.Mul(&scalar, mixer.BigInt(&mixerBigInt)) 526 expected.ScalarMultiplication(&g2Gen, &finalScalar) 527 528 // mixer ensures that all the words of a fpElement are set 529 var sampleScalars [nbSamples]fr.Element 530 531 for i := 1; i <= nbSamples; i++ { 532 sampleScalars[i-1].SetUint64(uint64(i)). 533 Mul(&sampleScalars[i-1], &mixer) 534 } 535 536 results := make([]G2Jac, len(cRange)) 537 for i, c := range cRange { 538 _innerMsmG2(&results[i], c, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 539 } 540 for i := 1; i < len(results); i++ { 541 if !results[i].Equal(&results[i-1]) { 542 t.Logf("result for c=%d != c=%d", cRange[i-1], cRange[i]) 543 return false 544 } 545 } 546 return true 547 }, 548 genScalar, 549 )) 550 551 properties.Property(fmt.Sprintf("[G2] Multi exponentiation (c in %v) of points at infinity should output a point at infinity", cRange), prop.ForAll( 552 func(mixer fr.Element) bool { 553 554 var samplePointsZero [nbSamples]G2Affine 555 556 var expected G2Jac 557 558 // compute expected result with double and add 559 var finalScalar, mixerBigInt big.Int 560 finalScalar.Mul(&scalar, mixer.BigInt(&mixerBigInt)) 561 expected.ScalarMultiplication(&g2Gen, &finalScalar) 562 563 // mixer ensures that all the words of a fpElement are set 564 var sampleScalars [nbSamples]fr.Element 565 566 for i := 1; i <= nbSamples; i++ { 567 sampleScalars[i-1].SetUint64(uint64(i)). 568 Mul(&sampleScalars[i-1], &mixer) 569 samplePointsZero[i-1].setInfinity() 570 } 571 572 results := make([]G2Jac, len(cRange)) 573 for i, c := range cRange { 574 _innerMsmG2(&results[i], c, samplePointsZero[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 575 } 576 for i := 0; i < len(results); i++ { 577 if !results[i].Z.IsZero() { 578 t.Logf("result for c=%d is not infinity", cRange[i]) 579 return false 580 } 581 } 582 return true 583 }, 584 genScalar, 585 )) 586 587 properties.Property(fmt.Sprintf("[G2] Multi exponentiation (c in %v) with a vector of 0s as input should output a point at infinity", cRange), prop.ForAll( 588 func(mixer fr.Element) bool { 589 // mixer ensures that all the words of a fpElement are set 590 var sampleScalars [nbSamples]fr.Element 591 592 results := make([]G2Jac, len(cRange)) 593 for i, c := range cRange { 594 _innerMsmG2(&results[i], c, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 595 } 596 for i := 0; i < len(results); i++ { 597 if !results[i].Z.IsZero() { 598 t.Logf("result for c=%d is not infinity", cRange[i]) 599 return false 600 } 601 } 602 return true 603 }, 604 genScalar, 605 )) 606 607 // note : this test is here as we expect to have a different multiExp than the above bucket method 608 // for small number of points 609 properties.Property("[G2] Multi exponentiation (<50points) should be consistent with sum of square", prop.ForAll( 610 func(mixer fr.Element) bool { 611 612 var g G2Jac 613 g.Set(&g2Gen) 614 615 // mixer ensures that all the words of a fpElement are set 616 samplePoints := make([]G2Affine, 30) 617 sampleScalars := make([]fr.Element, 30) 618 619 for i := 1; i <= 30; i++ { 620 sampleScalars[i-1].SetUint64(uint64(i)). 621 Mul(&sampleScalars[i-1], &mixer) 622 samplePoints[i-1].FromJacobian(&g) 623 g.AddAssign(&g2Gen) 624 } 625 626 var op1MultiExp G2Affine 627 op1MultiExp.MultiExp(samplePoints, sampleScalars, ecc.MultiExpConfig{}) 628 629 var finalBigScalar fr.Element 630 var finalBigScalarBi big.Int 631 var op1ScalarMul G2Affine 632 finalBigScalar.SetUint64(9455).Mul(&finalBigScalar, &mixer) 633 finalBigScalar.BigInt(&finalBigScalarBi) 634 op1ScalarMul.ScalarMultiplication(&g2GenAff, &finalBigScalarBi) 635 636 return op1ScalarMul.Equal(&op1MultiExp) 637 }, 638 genScalar, 639 )) 640 641 properties.TestingRun(t, gopter.ConsoleReporter(false)) 642 } 643 644 func TestCrossMultiExpG2(t *testing.T) { 645 const nbSamples = 1 << 14 646 // multi exp points 647 var samplePoints [nbSamples]G2Affine 648 var g G2Jac 649 g.Set(&g2Gen) 650 for i := 1; i <= nbSamples; i++ { 651 samplePoints[i-1].FromJacobian(&g) 652 g.AddAssign(&g2Gen) 653 } 654 655 // sprinkle some points at infinity 656 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 657 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 658 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 659 samplePoints[rand.N(nbSamples)].setInfinity() //#nosec G404 weak rng is fine here 660 661 var sampleScalars [nbSamples]fr.Element 662 fillBenchScalars(sampleScalars[:]) 663 664 // sprinkle some doublings 665 for i := 10; i < 100; i++ { 666 samplePoints[i] = samplePoints[0] 667 sampleScalars[i] = sampleScalars[0] 668 } 669 670 // cRange is generated from template and contains the available parameters for the multiexp window size 671 // for g2, CI suffers with large c size since it needs to allocate a lot of memory for the buckets. 672 // test only "odd" and "even" (ie windows size divide word size vs not) 673 cRange := []uint64{5, 14} 674 675 results := make([]G2Jac, len(cRange)) 676 for i, c := range cRange { 677 _innerMsmG2(&results[i], c, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 678 } 679 680 var r G2Jac 681 _innerMsmG2Reference(&r, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) 682 683 var expected, got G2Affine 684 expected.FromJacobian(&r) 685 686 for i := 0; i < len(results); i++ { 687 got.FromJacobian(&results[i]) 688 if !expected.Equal(&got) { 689 t.Fatalf("cross msm failed with c=%d", cRange[i]) 690 } 691 } 692 693 } 694 695 // _innerMsmG2Reference always do ext jacobian with c == 16 696 func _innerMsmG2Reference(p *G2Jac, points []G2Affine, scalars []fr.Element, config ecc.MultiExpConfig) *G2Jac { 697 // partition the scalars 698 digits, _ := partitionScalars(scalars, 16, config.NbTasks) 699 700 nbChunks := computeNbChunks(16) 701 702 // for each chunk, spawn one go routine that'll loop through all the scalars in the 703 // corresponding bit-window 704 // note that buckets is an array allocated on the stack and this is critical for performance 705 706 // each go routine sends its result in chChunks[i] channel 707 chChunks := make([]chan g2JacExtended, nbChunks) 708 for i := 0; i < len(chChunks); i++ { 709 chChunks[i] = make(chan g2JacExtended, 1) 710 } 711 712 // the last chunk may be processed with a different method than the rest, as it could be smaller. 713 n := len(points) 714 for j := int(nbChunks - 1); j >= 0; j-- { 715 processChunk := processChunkG2Jacobian[bucketg2JacExtendedC16] 716 go processChunk(uint64(j), chChunks[j], 16, points, digits[j*n:(j+1)*n], nil) 717 } 718 719 return msmReduceChunkG2Affine(p, int(16), chChunks[:]) 720 } 721 722 func BenchmarkMultiExpG2(b *testing.B) { 723 724 const ( 725 pow = (bits.UintSize / 2) - (bits.UintSize / 8) // 24 on 64 bits arch, 12 on 32 bits 726 nbSamples = 1 << pow 727 ) 728 729 var ( 730 samplePoints [nbSamples]G2Affine 731 sampleScalars [nbSamples]fr.Element 732 sampleScalarsSmallValues [nbSamples]fr.Element 733 sampleScalarsRedundant [nbSamples]fr.Element 734 ) 735 736 fillBenchScalars(sampleScalars[:]) 737 copy(sampleScalarsSmallValues[:], sampleScalars[:]) 738 copy(sampleScalarsRedundant[:], sampleScalars[:]) 739 740 // this means first chunk is going to have more work to do and should be split into several go routines 741 for i := 0; i < len(sampleScalarsSmallValues); i++ { 742 if i%5 == 0 { 743 sampleScalarsSmallValues[i].SetZero() 744 sampleScalarsSmallValues[i][0] = 1 745 } 746 } 747 748 // bad case for batch affine because scalar distribution might look uniform 749 // but over batchSize windows, we may hit a lot of conflicts and force the msm-affine 750 // to process small batches of additions to flush its queue of conflicted points. 751 for i := 0; i < len(sampleScalarsRedundant); i += 100 { 752 for j := i + 1; j < i+100 && j < len(sampleScalarsRedundant); j++ { 753 sampleScalarsRedundant[j] = sampleScalarsRedundant[i] 754 } 755 } 756 757 fillBenchBasesG2(samplePoints[:]) 758 759 var testPoint G2Affine 760 761 for i := 5; i <= pow; i++ { 762 using := 1 << i 763 764 b.Run(fmt.Sprintf("%d points", using), func(b *testing.B) { 765 b.ResetTimer() 766 for j := 0; j < b.N; j++ { 767 testPoint.MultiExp(samplePoints[:using], sampleScalars[:using], ecc.MultiExpConfig{}) 768 } 769 }) 770 771 b.Run(fmt.Sprintf("%d points-smallvalues", using), func(b *testing.B) { 772 b.ResetTimer() 773 for j := 0; j < b.N; j++ { 774 testPoint.MultiExp(samplePoints[:using], sampleScalarsSmallValues[:using], ecc.MultiExpConfig{}) 775 } 776 }) 777 778 b.Run(fmt.Sprintf("%d points-redundancy", using), func(b *testing.B) { 779 b.ResetTimer() 780 for j := 0; j < b.N; j++ { 781 testPoint.MultiExp(samplePoints[:using], sampleScalarsRedundant[:using], ecc.MultiExpConfig{}) 782 } 783 }) 784 } 785 } 786 787 func BenchmarkMultiExpG2Reference(b *testing.B) { 788 const nbSamples = 1 << 20 789 790 var ( 791 samplePoints [nbSamples]G2Affine 792 sampleScalars [nbSamples]fr.Element 793 ) 794 795 fillBenchScalars(sampleScalars[:]) 796 fillBenchBasesG2(samplePoints[:]) 797 798 var testPoint G2Affine 799 800 b.ResetTimer() 801 for j := 0; j < b.N; j++ { 802 testPoint.MultiExp(samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{}) 803 } 804 } 805 806 func BenchmarkManyMultiExpG2Reference(b *testing.B) { 807 const nbSamples = 1 << 20 808 809 var ( 810 samplePoints [nbSamples]G2Affine 811 sampleScalars [nbSamples]fr.Element 812 ) 813 814 fillBenchScalars(sampleScalars[:]) 815 fillBenchBasesG2(samplePoints[:]) 816 817 var t1, t2, t3 G2Affine 818 b.ResetTimer() 819 for j := 0; j < b.N; j++ { 820 var wg sync.WaitGroup 821 wg.Add(3) 822 go func() { 823 t1.MultiExp(samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{}) 824 wg.Done() 825 }() 826 go func() { 827 t2.MultiExp(samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{}) 828 wg.Done() 829 }() 830 go func() { 831 t3.MultiExp(samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{}) 832 wg.Done() 833 }() 834 wg.Wait() 835 } 836 } 837 838 // WARNING: this return points that are NOT on the curve and is meant to be use for benchmarking 839 // purposes only. We don't check that the result is valid but just measure "computational complexity". 840 // 841 // Rationale for generating points that are not on the curve is that for large benchmarks, generating 842 // a vector of different points can take minutes. Using the same point or subset will bias the benchmark result 843 // since bucket additions in extended jacobian coordinates will hit doubling algorithm instead of add. 844 func fillBenchBasesG2(samplePoints []G2Affine) { 845 var r big.Int 846 r.SetString("340444420969191673093399857471996460938405", 10) 847 samplePoints[0].ScalarMultiplication(&samplePoints[0], &r) 848 849 one := samplePoints[0].X 850 one.SetOne() 851 852 for i := 1; i < len(samplePoints); i++ { 853 samplePoints[i].X.Add(&samplePoints[i-1].X, &one) 854 samplePoints[i].Y.Sub(&samplePoints[i-1].Y, &one) 855 } 856 } 857 858 func fillBenchScalars(sampleScalars []fr.Element) { 859 // ensure every words of the scalars are filled 860 for i := 0; i < len(sampleScalars); i++ { 861 sampleScalars[i].SetRandom() 862 } 863 }