Skip to content

Commit b309ba4

Browse files
committed
secp256k1: Optimize NAF conversion.
This significantly optimizes the NAF conversion code by rewriting it to avoid all heap allocations as well as switch to an O(1) algorithm. The following benchmark shows a before and after comparison of the NAF conversion as well as how that translates to scalar multiplication and signature verification: name old time/op new time/op delta ---------------------------------------------------------------------- NAF 1.16µs ± 1% 0.08µs ± 1% -93.02% (p=0.008 n=5+5) ScalarMult 138µs ± 1% 135µs ± 0% -1.77% (p=0.016 n=5+4) SigVerify 164µs ± 0% 162µs ± 0% -0.98% (p=0.008 n=5+5) name old alloc/op new alloc/op delta ---------------------------------------------------------------------- NAF 96.0B ± 0% 0.0B -100.00% (p=0.008 n=5+5) ScalarMult 816B ± 0% 720B ± 0% -11.76% (p=0.008 n=5+5) SigVerify 1.54kB ± 0% 1.44kB ± 0% -6.25% (p=0.008 n=5+5) name old allocs/op new allocs/op delta ---------------------------------------------------------------------- NAF 2.00 ± 0% 0.00 -100.00% (p=0.008 n=5+5) ScalarMult 15.0 ± 0% 11.0 ± 0% -26.67% (p=0.008 n=5+5) SigVerify 32.0 ± 0% 28.0 ± 0% -12.50% (p=0.008 n=5+5)
1 parent fb507c1 commit b309ba4

File tree

2 files changed

+146
-86
lines changed

2 files changed

+146
-86
lines changed

dcrec/secp256k1/curve.go

+142-84
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import (
1515
// https://www.secg.org/sec2-v2.pdf
1616
//
1717
// [GECC]: Guide to Elliptic Curve Cryptography (Hankerson, Menezes, Vanstone)
18+
//
19+
// [BRID]: On Binary Representations of Integers with Digits -1, 0, 1
20+
// (Prodinger, Helmut)
1821

1922
// All group operations are performed using Jacobian coordinates. For a given
2023
// (x, y) position on the curve, the Jacobian coordinates are (x1, y1, z1)
@@ -636,6 +639,142 @@ func splitK(k []byte) ([]byte, []byte, int, int) {
636639
return k1.Bytes(), k2.Bytes(), k1.Sign(), k2.Sign()
637640
}
638641

642+
// nafScalar represents a positive integer up to a maximum value of 2^256 - 1
643+
// encoded in non-adjacent form.
644+
//
645+
// NAF is a signed-digit representation where each digit can be +1, 0, or -1.
646+
//
647+
// In order to efficiently encode that information, this type uses two arrays, a
648+
// "positive" array where set bits represent the +1 signed digits and a
649+
// "negative" array where set bits represent the -1 signed digits. 0 is
650+
// represented by neither array having a bit set in that position.
651+
//
652+
// The Pos and Neg methods return the aforementioned positive and negative
653+
// arrays, respectively.
654+
type nafScalar struct {
655+
// pos houses the positive portion of the representation. An additional
656+
// byte is required for the positive portion because the NAF encoding can be
657+
// up to 1 bit longer than the normal binary encoding of the value.
658+
//
659+
// neg houses the negative portion of the representation. Even though the
660+
// additional byte is not required for the negative portion, since it can
661+
// never exceed the length of the normal binary encoding of the value,
662+
// keeping the same length for positive and negative portions simplifies
663+
// working with the representation and allows extra conditional branches to
664+
// be avoided.
665+
//
666+
// start and end specify the starting and ending index to use within the pos
667+
// and neg arrays, respectively. This allows fixed size arrays to be used
668+
// versus needing to dynamically allocate space on the heap.
669+
//
670+
// NOTE: The fields are defined in the order that they are to minimize the
671+
// padding on 32-bit and 64-bit platforms.
672+
pos [33]byte
673+
start, end uint8
674+
neg [33]byte
675+
}
676+
677+
// Pos returns the bytes of the encoded value with bits set in the positions
678+
// that represent a signed digit of +1.
679+
func (s *nafScalar) Pos() []byte {
680+
return s.pos[s.start:s.end]
681+
}
682+
683+
// Neg returns the bytes of the encoded value with bits set in the positions
684+
// that represent a signed digit of -1.
685+
func (s *nafScalar) Neg() []byte {
686+
return s.neg[s.start:s.end]
687+
}
688+
689+
// naf takes a positive integer up to a maximum value of 2^256 - 1 and returns
690+
// its non-adjacent form (NAF), which is a unique signed-digit representation
691+
// such that no two consecutive digits are nonzero. See the documentation for
692+
// the returned type for details on how the representation is encoded
693+
// efficiently and how to interpret it
694+
//
695+
// NAF is useful in that it has the fewest nonzero digits of any signed digit
696+
// representation, only 1/3rd of its digits are nonzero on average, and at least
697+
// half of the digits will be 0.
698+
//
699+
// The aforementioned properties are particularly beneficial for optimizing
700+
// elliptic curve point multiplication because they effectively minimize the
701+
// number of required point additions in exchange for needing to perform a mix
702+
// of fewer point additions and subtractions and possibly one additional point
703+
// doubling. This is an excellent tradeoff because subtraction of points has
704+
// the same computational complexity as addition of points and point doubling is
705+
// faster than both.
706+
func naf(k []byte) nafScalar {
707+
// Strip leading zero bytes.
708+
for len(k) > 0 && k[0] == 0x00 {
709+
k = k[1:]
710+
}
711+
712+
// The non-adjacent form (NAF) of a positive integer k is an expression
713+
// k = ∑_(i=0, l-1) k_i * 2^i where k_i ∈ {0,±1}, k_(l-1) != 0, and no two
714+
// consecutive digits k_i are nonzero.
715+
//
716+
// The traditional method of computing the NAF of a positive integer is
717+
// given by algorithm 3.30 in [GECC]. It consists of repeatedly dividing k
718+
// by 2 and choosing the remainder so that the quotient (k−r)/2 is even
719+
// which ensures the next NAF digit is 0. This requires log_2(k) steps.
720+
//
721+
// However, in [BRID], Prodinger notes that a closed form expression for the
722+
// NAF representation is the bitwise difference 3k/2 - k/2. This is more
723+
// efficient as it can be computed in O(1) versus the O(log(n)) of the
724+
// traditional approach.
725+
//
726+
// The following code makes use of that formula to compute the NAF more
727+
// efficiently.
728+
//
729+
// To understand the logic here, observe that the only way the NAF has a
730+
// nonzero digit at a given bit is when either 3k/2 or k/2 has a bit set in
731+
// that position, but not both. In other words, the result of a bitwise
732+
// xor. This can be seen simply by considering that when the bits are the
733+
// same, the subtraction is either 0-0 or 1-1, both of which are 0.
734+
//
735+
// Further, observe that the "+1" digits in the result are contributed by
736+
// 3k/2 while the "-1" digits are from k/2. So, they can be determined by
737+
// taking the bitwise and of each respective value with the result of the
738+
// xor which identifies which bits are nonzero.
739+
//
740+
// Using that information, this loops backwards from the least significant
741+
// byte to the most significant byte while performing the aforementioned
742+
// calculations by propagating the potential carry and high order bit from
743+
// the next word during the right shift.
744+
kLen := len(k)
745+
var result nafScalar
746+
var carry uint8
747+
for byteNum := kLen - 1; byteNum >= 0; byteNum-- {
748+
// Calculate k/2. Notice the carry from the previous word is added and
749+
// the low order bit from the next word is shifted in accordingly.
750+
kc := uint16(k[byteNum]) + uint16(carry)
751+
var nextWord uint8
752+
if byteNum > 0 {
753+
nextWord = k[byteNum-1]
754+
}
755+
halfK := kc>>1 | uint16(nextWord<<7)
756+
757+
// Calculate 3k/2 and determine the non-zero digits in the result.
758+
threeHalfK := kc + halfK
759+
nonZeroResultDigits := threeHalfK ^ halfK
760+
761+
// Determine the signed digits {0, ±1}.
762+
result.pos[byteNum+1] = uint8(threeHalfK & nonZeroResultDigits)
763+
result.neg[byteNum+1] = uint8(halfK & nonZeroResultDigits)
764+
765+
// Propagate the potential carry from the 3k/2 calculation.
766+
carry = uint8(threeHalfK >> 8)
767+
}
768+
result.pos[0] = carry
769+
770+
// Set the starting and ending positions within the fixed size arrays to
771+
// identify the bytes that are actually used. This is important since the
772+
// encoding is big endian and thus trailing zero bytes changes its value.
773+
result.start = 1 - carry
774+
result.end = uint8(kLen + 1)
775+
return result
776+
}
777+
639778
// ScalarMultNonConst multiplies k*P where k is a big endian integer modulo the
640779
// curve order and P is a point in Jacobian projective coordinates and stores
641780
// the result in the provided Jacobian point.
@@ -683,8 +822,9 @@ func ScalarMultNonConst(k *ModNScalar, point, result *JacobianPoint) {
683822
//
684823
// The Pos version of the bytes contain the +1s and the Neg versions
685824
// contain the -1s.
686-
k1PosNAF, k1NegNAF := naf(k1)
687-
k2PosNAF, k2NegNAF := naf(k2)
825+
k1NAF, k2NAF := naf(k1), naf(k2)
826+
k1PosNAF, k1NegNAF := k1NAF.Pos(), k1NAF.Neg()
827+
k2PosNAF, k2NegNAF := k2NAF.Pos(), k2NAF.Neg()
688828
k1Len, k2Len := len(k1PosNAF), len(k2PosNAF)
689829

690830
m := k1Len
@@ -770,88 +910,6 @@ func ScalarBaseMultNonConst(k *ModNScalar, result *JacobianPoint) {
770910
result.Set(&q)
771911
}
772912

773-
// naf takes a positive integer k and returns the Non-Adjacent Form (NAF) as two
774-
// byte slices. The first is where 1s will be. The second is where -1s will
775-
// be. NAF is convenient in that on average, only 1/3rd of its values are
776-
// non-zero. This is algorithm 3.30 from [GECC].
777-
//
778-
// Essentially, this makes it possible to minimize the number of operations
779-
// since the resulting ints returned will be at least 50% 0s.
780-
func naf(k []byte) ([]byte, []byte) {
781-
// Strip leading zero bytes.
782-
for len(k) > 0 && k[0] == 0x00 {
783-
k = k[1:]
784-
}
785-
786-
// The essence of this algorithm is that whenever we have consecutive 1s
787-
// in the binary, we want to put a -1 in the lowest bit and get a bunch
788-
// of 0s up to the highest bit of consecutive 1s. This is due to this
789-
// identity:
790-
// 2^n + 2^(n-1) + 2^(n-2) + ... + 2^(n-k) = 2^(n+1) - 2^(n-k)
791-
//
792-
// The algorithm thus may need to go 1 more bit than the length of the
793-
// bits we actually have, hence bits being 1 bit longer than was
794-
// necessary. Since we need to know whether adding will cause a carry,
795-
// we go from right-to-left in this addition.
796-
var carry, curIsOne, nextIsOne bool
797-
// these default to zero
798-
retPos := make([]byte, len(k)+1)
799-
retNeg := make([]byte, len(k)+1)
800-
for i := len(k) - 1; i >= 0; i-- {
801-
curByte := k[i]
802-
for j := uint(0); j < 8; j++ {
803-
curIsOne = curByte&1 == 1
804-
if j == 7 {
805-
if i == 0 {
806-
nextIsOne = false
807-
} else {
808-
nextIsOne = k[i-1]&1 == 1
809-
}
810-
} else {
811-
nextIsOne = curByte&2 == 2
812-
}
813-
if carry {
814-
if curIsOne {
815-
// This bit is 1, so continue to carry
816-
// and don't need to do anything.
817-
} else {
818-
// We've hit a 0 after some number of
819-
// 1s.
820-
if nextIsOne {
821-
// Start carrying again since
822-
// a new sequence of 1s is
823-
// starting.
824-
retNeg[i+1] += 1 << j
825-
} else {
826-
// Stop carrying since 1s have
827-
// stopped.
828-
carry = false
829-
retPos[i+1] += 1 << j
830-
}
831-
}
832-
} else if curIsOne {
833-
if nextIsOne {
834-
// If this is the start of at least 2
835-
// consecutive 1s, set the current one
836-
// to -1 and start carrying.
837-
retNeg[i+1] += 1 << j
838-
carry = true
839-
} else {
840-
// This is a singleton, not consecutive
841-
// 1s.
842-
retPos[i+1] += 1 << j
843-
}
844-
}
845-
curByte >>= 1
846-
}
847-
}
848-
if carry {
849-
retPos[0] = 1
850-
return retPos, retNeg
851-
}
852-
return retPos[1:], retNeg[1:]
853-
}
854-
855913
// isOnCurve returns whether or not the affine point (x,y) is on the curve.
856914
func isOnCurve(fx, fy *FieldVal) bool {
857915
// Elliptic curve equation for secp256k1 is: y^2 = x^3 + 7

dcrec/secp256k1/curve_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,8 @@ func TestNAF(t *testing.T) {
611611
// Ensure the resulting positive and negative portions of the overall
612612
// NAF representation adhere to the requirements of NAF encoding and
613613
// they sum back to the original value.
614-
pos, neg := naf(hexToBytes(test.in))
614+
result := naf(hexToBytes(test.in))
615+
pos, neg := result.Pos(), result.Neg()
615616
if err := checkNAFEncoding(pos, neg, fromHex(test.in)); err != nil {
616617
t.Errorf("%q: %v", test.name, err)
617618
}
@@ -636,7 +637,8 @@ func TestNAFRandom(t *testing.T) {
636637
// they sum back to the original value.
637638
bigIntVal, modNVal := randIntAndModNScalar(t, rng)
638639
valBytes := modNVal.Bytes()
639-
pos, neg := naf(valBytes[:])
640+
result := naf(valBytes[:])
641+
pos, neg := result.Pos(), result.Neg()
640642
if err := checkNAFEncoding(pos, neg, bigIntVal); err != nil {
641643
t.Fatalf("encoding err: %v\nin: %x\npos: %x\nneg: %x", err,
642644
bigIntVal, pos, neg)

0 commit comments

Comments
 (0)