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  }