github.com/consensys/gnark-crypto@v0.14.0/ecc/secp256k1/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 secp256k1 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/secp256k1/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} 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} 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 == 15 286 func _innerMsmG1Reference(p *G1Jac, points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) *G1Jac { 287 // partition the scalars 288 digits, _ := partitionScalars(scalars, 15, config.NbTasks) 289 290 nbChunks := computeNbChunks(15) 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[bucketg1JacExtendedC15] 306 go processChunk(uint64(j), chChunks[j], 15, points, digits[j*n:(j+1)*n], nil) 307 } 308 309 return msmReduceChunkG1Affine(p, int(15), 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 fillBenchScalars(sampleScalars []fr.Element) { 449 // ensure every words of the scalars are filled 450 for i := 0; i < len(sampleScalars); i++ { 451 sampleScalars[i].SetRandom() 452 } 453 }