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  }