Skip to content

Commit

Permalink
secp256k1: Optimize k splitting with mod n scalar.
Browse files Browse the repository at this point in the history
This optimizes the scalar decomposition code by rewriting it to make use
of the highly-efficient zero-allocation ModNScalar type along with
math/bits (which provides hardware acceleration on most platforms)
instead of big.Ints.

The net effect is that the decomposition is significantly faster,
allocation free, and constant time.

It also adds a bunch of detailed comments to better describe the
endomorphism and how it is used in scalar multiplication in addition to
fully describing the scalar decomposition process and derivation.

Finally, it adds logic to derive and print the new constants the
reworked code makes of to the precomputation code that runs with 'go
generate'.

The following is a high level overview of the changes:
- Rewrites splitK to use the ModNScalar type instead of big.Ints:
  - Allocation free
  - Constant time
  - Provides hardware acceleration on most platforms
  - Includes detailed comments describing the scalar decomposition
    process and derivation
- Updates endomorphism parameter constant definitions to be ModNScalars
  instead of big.Int
  - Moves the convenience func hexToModNScalar to the main package
- Adds new endoZ1 and endoZ2 constants for the rounded multiplication
  used by the new decomposition code
  - Adds logic to derive and print the new endomorphism constants with
    'go generate'
- Updates the scalar multiplication to account for the new semantics
- Adds detailed comments to scalar multiplication
- Tightens negation magnitudes in addZ1EqualsZ2 and remove no longer
  needed normalization
- Ensures the calculation when recovering compact signatures uses
  normalized points
- Updates associated tests
- Updates associated benchmark
- Removes splitKModN test helper since conversion is no longer needed

The following benchmarks show a before and after comparison of scalar
decomposition as well as how it that translates to scalar multiplication
and signature verification:

name         old time/op    new time/op     delta
---------------------------------------------------------------------
SplitK       1.61µs ±32%    0.89µs ± 2%     -44.69% (p=0.000 n=50+47)
ScalarMult   125µs ± 1%     115µs ± 1%      -7.82%  (p=0.000 n=43+46)
SigVerify    161µs ±25%     160µs ±19%      -0.53%  (p=0.001 n=50+50)

name         old allocs/op  new allocs/op   delta
-----------------------------------------------------------------------
SplitK       10.0 ± 0%       0.0            -100.00%  (p=0.000 n=50+50)
ScalarMult   11.0 ± 0%       0.0            -100.00%  (p=0.000 n=50+50)
SigVerify    28.0 ± 0%      16.0 ± 0%       -42.86%   (p=0.000 n=50+50)

While it only saves about 1 µs per signature verification in the
benchmarking scenario, the primary win is the reduction in the number of
allocations per signature verification which has a much more significant
impact when verifying large numbers of signatures back to back, such as
when processing new blocks, and especially during the initial chain sync
process.
  • Loading branch information
davecgh committed Mar 10, 2022
1 parent e14dd59 commit 8399238
Show file tree
Hide file tree
Showing 5 changed files with 461 additions and 165 deletions.
13 changes: 4 additions & 9 deletions dcrec/secp256k1/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func BenchmarkAddNonConstNotZOne(b *testing.B) {
// BenchmarkScalarBaseMultNonConst benchmarks multiplying a scalar by the base
// point of the curve.
func BenchmarkScalarBaseMultNonConst(b *testing.B) {
k := new(ModNScalar).SetHex("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575")
k := hexToModNScalar("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575")

b.ReportAllocs()
b.ResetTimer()
Expand Down Expand Up @@ -84,7 +84,7 @@ func BenchmarkSplitK(b *testing.B) {
halfOrderPOneMLambda := new(ModNScalar).Add2(halfOrderPOne, negLambda)
lambdaPHalfOrder := new(ModNScalar).Add2(endoLambda, halfOrder)
lambdaPOnePHalfOrder := new(ModNScalar).Add2(lambdaPOne, halfOrder)
scalarsN := []*ModNScalar{
scalars := []*ModNScalar{
new(ModNScalar), // zero
oneModN, // one
negOne, // group order - 1 (aka -1 mod N)
Expand All @@ -100,25 +100,20 @@ func BenchmarkSplitK(b *testing.B) {
lambdaPHalfOrder, // lambda + group half order
lambdaPOnePHalfOrder, // lambda + 1 + group half order
}
scalars := make([][]byte, len(scalarsN))
for i, scalar := range scalarsN {
b := scalar.Bytes()
scalars[i] = b[:]
}

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i += len(scalars) {
for j := 0; j < len(scalars); j++ {
_, _, _, _ = splitK(scalars[j])
_, _ = splitK(scalars[j])
}
}
}

// BenchmarkScalarMultNonConst benchmarks multiplying a scalar by an arbitrary
// point on the curve.
func BenchmarkScalarMultNonConst(b *testing.B) {
k := new(ModNScalar).SetHex("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575")
k := hexToModNScalar("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575")
point := jacobianPointFromHex(
"34f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6",
"0b71ea9bd730fd8923f6d25a7a91e7dd7728a960686cb5a901bb419e0f2ca232",
Expand Down
Loading

0 comments on commit 8399238

Please sign in to comment.