From 7c71edf8e69732692dae49f34db44a95cbc5673e Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 9 Jul 2026 01:28:21 -0500 Subject: [PATCH 01/33] secp256k1: Add caveats to documentation. This updates the README.md and doc.go files to include some important caveats about the constant time and memory clearing behavior. Specifically, while the behavior has been manually verified by carefully reviewing the generated assembly as of the most recent version of Go, it is impossible to guarantee the compiler will not change in the future. --- dcrec/secp256k1/README.md | 19 +++++++++++++++++-- dcrec/secp256k1/doc.go | 15 +++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/dcrec/secp256k1/README.md b/dcrec/secp256k1/README.md index 53f3839117..4fcfa18616 100644 --- a/dcrec/secp256k1/README.md +++ b/dcrec/secp256k1/README.md @@ -16,8 +16,8 @@ structures and functions for working with public and private secp256k1 keys. In addition, subpackages are provided to produce, verify, parse, and serialize ECDSA signatures and EC-Schnorr-DCRv0 (a custom Schnorr-based signature scheme -specific to Decred) signatures. See the README.md files in the relevant sub -packages for more details about those aspects. +specific to Decred) signatures. See the README.md files in the relevant +subpackages for more details about those aspects. ## Features @@ -95,6 +95,21 @@ secp256k1 elliptic curve cryptography. The secp256k1 domain parameters are specified in [SEC 2: Recommended Elliptic Curve Domain Parameters](https://www.secg.org/sec2-v2.pdf). +## Caveats + +Constant time implementations are designed to eliminate data dependent timing +variation at the algorithmic level. This property depends on the Go compiler +continuing to compile the relevant operations into constant-time machine +instructions. + +This package also consistently uses temporary stack allocated buffers which it +attempts to clear in order to reduce the likelihood of sensitive data lingering +in memory, but Go does not guarantee such writes are never optimized away. + +Significant effort has been made to ensure correctness including careful review +of the generated assembly, but it is impossible to guarantee exact behavior of +future versions of the Go compiler. + ## secp256k1 use in Decred At the time of this writing, the primary public key cryptography in widespread diff --git a/dcrec/secp256k1/doc.go b/dcrec/secp256k1/doc.go index d946650d73..69121d1164 100644 --- a/dcrec/secp256k1/doc.go +++ b/dcrec/secp256k1/doc.go @@ -50,6 +50,21 @@ use optimized secp256k1 elliptic curve cryptography. Finally, a comprehensive suite of tests is provided to provide a high level of quality assurance. +## Caveats + +Constant time implementations are designed to eliminate data dependent timing +variation at the algorithmic level. This property depends on the Go compiler +continuing to compile the relevant operations into constant time machine +instructions. + +This package also consistently uses temporary stack allocated buffers which it +attempts to clear in order to reduce the likelihood of sensitive data lingering +in memory, but Go does not guarantee such writes are never optimized away. + +Significant effort has been made to ensure correctness, including careful review +of the generated assembly, but it is impossible to guarantee exact behavior of +future versions of the Go compiler. + # Use of secp256k1 in Decred At the time of this writing, the primary public key cryptography in widespread From 80ed947f789c14ea9ba231f1a21e9eb96ab176a5 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 9 Jul 2026 03:35:47 -0500 Subject: [PATCH 02/33] secp256k1: Add scalar negation algebraic tests. This adds a new test helper for some algebraic properties of scalar negation and calls it from the test that handles specific edge cases as well as the randomized test. --- dcrec/secp256k1/modnscalar_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/dcrec/secp256k1/modnscalar_test.go b/dcrec/secp256k1/modnscalar_test.go index 251708a6ff..b894e9b4ce 100644 --- a/dcrec/secp256k1/modnscalar_test.go +++ b/dcrec/secp256k1/modnscalar_test.go @@ -979,6 +979,26 @@ func TestModNScalarSquareRandom(t *testing.T) { } } +// checkModNScalarNegateProps checks algebraic properties of scalar negation. +func checkModNScalarNegateProps(t *testing.T, val *ModNScalar) { + t.Helper() + + negation := new(ModNScalar).NegateVal(val) + + // Ensure involution produces the original value. That is -(-x) == x. + involution := new(ModNScalar).Set(negation).Negate() + if !involution.Equals(val) { + t.Errorf("mismatched involution -- got: %v, want: %v", involution, + val) + } + + // Ensure x + (-x) == 0. + sum := new(ModNScalar).Add2(val, negation) + if !sum.IsZero() { + t.Errorf("x + (-x) != 0 -- got: %v, want: 0", sum) + } +} + // TestModNScalarNegate ensures that negating scalars works as expected for edge // cases. func TestModNScalarNegate(t *testing.T) { @@ -1051,6 +1071,9 @@ func TestModNScalarNegate(t *testing.T) { result2, expected) continue } + + // Ensure algebraic properties with negation hold. + checkModNScalarNegateProps(t, s) } } @@ -1086,6 +1109,9 @@ func TestModNScalarNegateRandom(t *testing.T) { "big int result: %x\nscalar result %v", bigIntVal, modNVal, bigIntResult, modNValResult) } + + // Ensure algebraic properties with negation hold. + assertModNScalarNegateProperties(t, modNVal) } } From 0821659355e8f65212358eb0aec129de78af163f Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 9 Jul 2026 04:10:05 -0500 Subject: [PATCH 03/33] secp256k1: Add scalar inverse algebraic tests. This adds a new test helper for some algebraic properties of modular multiplicative inverses of scalars and calls it from the test that handles specific edge cases as well as the randomized test. --- dcrec/secp256k1/modnscalar_test.go | 34 +++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/dcrec/secp256k1/modnscalar_test.go b/dcrec/secp256k1/modnscalar_test.go index b894e9b4ce..e4a9307b63 100644 --- a/dcrec/secp256k1/modnscalar_test.go +++ b/dcrec/secp256k1/modnscalar_test.go @@ -1111,7 +1111,33 @@ func TestModNScalarNegateRandom(t *testing.T) { } // Ensure algebraic properties with negation hold. - assertModNScalarNegateProperties(t, modNVal) + checkModNScalarNegateProps(t, modNVal) + } +} + +// checkModNScalarInverseProps checks algebraic properties of the +// multiplicative modular inverse of scalars. +func checkModNScalarInverseProps(t *testing.T, val *ModNScalar) { + t.Helper() + + // Exception for 0 which does not have a modular multiplicative inverse. + if val.IsZero() { + return + } + + inverse := new(ModNScalar).InverseValNonConst(val) + + // Ensure x * x^-1 ≡ 1 (mod N). + product := new(ModNScalar).Mul2(val, inverse) + one := new(ModNScalar).SetInt(1) + if !product.Equals(one) { + t.Errorf("x * x^-1 != 1 (mod N) -- got: %v, want: %v", product, one) + } + + // Ensure (x^-1)^-1 == x. + doubleInv := new(ModNScalar).InverseValNonConst(inverse) + if !doubleInv.Equals(val) { + t.Errorf("(x^-1)^-1 != x (mod N) -- got: %v, want: %v", doubleInv, val) } } @@ -1189,6 +1215,9 @@ func TestModNScalarInverseNonConst(t *testing.T) { result2, expected) continue } + + // Ensure algebraic properties with inverses hold. + checkModNScalarInverseProps(t, s) } } @@ -1224,6 +1253,9 @@ func TestModNScalarInverseNonConstRandom(t *testing.T) { "big int result: %x\nscalar result %v", bigIntVal, modNVal, bigIntResult, modNValResult) } + + // Ensure algebraic properties with inverses hold. + checkModNScalarInverseProps(t, modNVal) } } From 25d2550877fc4f7b7cd69a5c16d30908b4aad847 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 9 Jul 2026 04:30:46 -0500 Subject: [PATCH 04/33] secp256k1: Add scalar half order algebraic tests. This adds a new test helper for some algebraic properties of the scalar half order check and calls it from the test that handles specific edge cases as well as the randomized test. --- dcrec/secp256k1/modnscalar_test.go | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/dcrec/secp256k1/modnscalar_test.go b/dcrec/secp256k1/modnscalar_test.go index e4a9307b63..448378e0cf 100644 --- a/dcrec/secp256k1/modnscalar_test.go +++ b/dcrec/secp256k1/modnscalar_test.go @@ -1259,6 +1259,27 @@ func TestModNScalarInverseNonConstRandom(t *testing.T) { } } +// checkModNScalarHalfOrderProps checks algebraic properties of the half order +// comparison. +func checkModNScalarHalfOrderProps(t *testing.T, val *ModNScalar) { + t.Helper() + + // Exception for 0 which is the only scalar where negation does not produce + // a distinct value. + if val.IsZero() { + return + } + + // Ensure negating a scalar flips whether it is over half order. + // + // x > N/2 implies N-x < N/2 and vice versa. + negation := new(ModNScalar).NegateVal(val) + if val.IsOverHalfOrder() == negation.IsOverHalfOrder() { + t.Errorf("negation did not flip half order -- val: %v, negation: %v", + val, negation) + } +} + // TestModNScalarIsOverHalfOrder ensures that scalars report whether or not they // exceeed the half order works as expected for edge cases. func TestModNScalarIsOverHalfOrder(t *testing.T) { @@ -1305,12 +1326,16 @@ func TestModNScalarIsOverHalfOrder(t *testing.T) { }} for _, test := range tests { - result := mustModNScalarWithOverflow(test.in).IsOverHalfOrder() + s := mustModNScalarWithOverflow(test.in) + result := s.IsOverHalfOrder() if result != test.expected { t.Errorf("%s: unexpected result -- got: %v, want: %v", test.name, result, test.expected) continue } + + // Ensure algebraic properties with the half order hold. + checkModNScalarHalfOrderProps(t, s) } } @@ -1344,5 +1369,8 @@ func TestModNScalarIsOverHalfOrderRandom(t *testing.T) { "in: %v\nbig int result: %v\nscalar result %v", bigIntVal, modNVal, bigIntResult, modNValResult) } + + // Ensure algebraic properties with the half order hold. + checkModNScalarHalfOrderProps(t, modNVal) } } From e684578ae05ed27ea967a791331e92f8875de959 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 9 Jul 2026 01:28:29 -0500 Subject: [PATCH 05/33] secp256k1: Implement 4x64 ModNScalar. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This significantly optimizes the ModNScalar implementation by rewriting it to use saturated 4x64 arithmetic similar to the new 4x64 field arithmetic. The new implementation retains all of the important properties such as being constant time and only using temporary buffers on the stack that are cleared. Of particular interest is the new modular reduction implementation. It moves away from the 96-bit accumulate design and instead manually tracks and discards carries inline which provides a major speed advantage. Due to the complexity and potential mistakes in the arithmetic, all of the intermediate bounds and carry assumptions documented that the code relies upon have been formally verified to be correct. The formal proof is in a separate commit. The implementation is primarily targeted at 64-bit since it is by far the most common architecture in modern use, however, as the included benchmarks below show, it is faster for almost all operations on 32-bit architectures as well. The few operations where it is a bit slower on 32-bit architectures are not used anywhere near as frequently as the ones that get 10-30% better performance, so the end result is the overall implementation is notably faster on 32 bit too. For 64-bit architectures, every operation is significantly faster. For example, all of the primary operations that get heavy use get ~73-94% better performance. Finally, the tests have been updated to use values specifically crafted to exercise the new limbs versus the old values that were crafted for the old limbs. Comparison for 32-bit platforms: $ benchstat beforescalar_x86.txt afterscalar_x86.txt name old time/op new time/op delta ----------------------------------------------------------------------------------- ModNScalar 22.9ns ± 4% 23.2ns ± 5% ~ (p=0.184 n=10+10) ModNScalarZero 1.36ns ± 3% 6.59ns ± 6% +385.95% (p=0.000 n=9+9) ModNScalarIsZero 0.32ns ± 4% 0.34ns ±14% ~ (p=0.143 n=10+10) ModNScalarEquals 3.69ns ± 4% 0.30ns ± 7% -91.78% (p=0.000 n=10+10) ModNScalarAdd 25.1ns ± 6% 22.5ns ± 5% -10.40% (p=0.000 n=9+10) ModNScalarMul 257ns ± 5% 223ns ± 5% -13.07% (p=0.000 n=10+10) ModNScalarSquare 267ns ± 5% 172ns ± 6% -35.56% (p=0.000 n=10+10) ModNScalarNegate 12.8ns ± 9% 14.5ns ± 2% +13.39% (p=0.000 n=10+10) ModNScalarInverse 686ns ± 7% 623ns ± 3% -9.24% (p=0.000 n=10+9) ModNScalarIsOverHalfOrder 8.92ns ± 6% 6.55ns ± 3% -26.59% (p=0.000 n=10+10) Comparison for 64-bit platforms: $ benchstat beforescalar_x64.txt afterscalar_x64.txt name old time/op new time/op delta ModNScalar 12.1ns ± 1% 3.5ns ± 4% -71.25% (p=0.000 n=7+10) ModNScalarZero 0.64ns ± 9% 0.61ns ± 5% -4.08% (p=0.043 n=10+10) ModNScalarIsZero 0.31ns ± 8% 0.31ns ± 6% ~ (p=0.000 n=10+10) ModNScalarEquals 2.63ns ± 5% 0.33ns ± 9% -87.47% (p=0.000 n=10+10) ModNScalarAdd 15.1ns ± 4% 4.1ns ± 4% -73.25% (p=0.000 n=10+10) ModNScalarMul 214ns ± 6% 29ns ± 6% -86.30% (p=0.000 n=10+10) ModNScalarSquare 215ns ± 4% 23ns ± 6% -89.39% (p=0.000 n=10+10) ModNScalarNegate 5.78ns ± 4% 2.99ns ±10% -48.33% (p=0.000 n=10+10) ModNScalarInverse 452ns ± 4% 408ns ± 6% -9.89% (p=0.000 n=10+10) ModNScalarIsOverHalfOrder 5.52ns ± 6% 0.29ns ± 5% -94.66% (p=0.000 n=10+9) --- dcrec/secp256k1/common.go | 7 +- dcrec/secp256k1/curve.go | 54 +- dcrec/secp256k1/curve_test.go | 20 +- dcrec/secp256k1/field_test.go | 4 +- dcrec/secp256k1/modnscalar.go | 1359 ++++++++++++---------------- dcrec/secp256k1/modnscalar_test.go | 473 +++++----- 6 files changed, 881 insertions(+), 1036 deletions(-) diff --git a/dcrec/secp256k1/common.go b/dcrec/secp256k1/common.go index b45e013bd5..9ff782bf40 100644 --- a/dcrec/secp256k1/common.go +++ b/dcrec/secp256k1/common.go @@ -15,9 +15,10 @@ func constantTimeEq64(a, b uint64) uint32 { return uint32(((t | -t) >> 63) ^ 1) } -// constantTimeNotEq returns 1 if a != b or 0 otherwise in constant time. -func constantTimeNotEq(a, b uint32) uint32 { - return ^uint32((uint64(a^b)-1)>>63) & 1 +// constantTimeNotEq64 returns 1 if a != b or 0 otherwise in constant time. +func constantTimeNotEq64(a, b uint64) uint32 { + t := a ^ b + return uint32(((t | -t) >> 63)) } // constantTimeLess returns 1 if a < b or 0 otherwise in constant time. diff --git a/dcrec/secp256k1/curve.go b/dcrec/secp256k1/curve.go index 84c6d0c414..750b57a327 100644 --- a/dcrec/secp256k1/curve.go +++ b/dcrec/secp256k1/curve.go @@ -720,16 +720,6 @@ func mulAdd64Carry(digit1, digit2, m, c uint64) (hi, lo uint64) { // because it is used for replacing division with multiplication and thus the // intermediate results must be done via a field extension to a larger field. func mul512Rsh320Round(n1, n2 *ModNScalar) ModNScalar { - // Convert n1 and n2 to base 2^64 digits. - n1Digit0 := uint64(n1.n[0]) | uint64(n1.n[1])<<32 - n1Digit1 := uint64(n1.n[2]) | uint64(n1.n[3])<<32 - n1Digit2 := uint64(n1.n[4]) | uint64(n1.n[5])<<32 - n1Digit3 := uint64(n1.n[6]) | uint64(n1.n[7])<<32 - n2Digit0 := uint64(n2.n[0]) | uint64(n2.n[1])<<32 - n2Digit1 := uint64(n2.n[2]) | uint64(n2.n[3])<<32 - n2Digit2 := uint64(n2.n[4]) | uint64(n2.n[5])<<32 - n2Digit3 := uint64(n2.n[6]) | uint64(n2.n[7])<<32 - // Compute the full 512-bit product n1*n2. var r0, r1, r2, r3, r4, r5, r6, r7, c uint64 @@ -738,40 +728,40 @@ func mul512Rsh320Round(n1, n2 *ModNScalar) ModNScalar { // // Note that r0 is ignored because it is not needed to compute the higher // terms and it is shifted out below anyway. - c, _ = bits.Mul64(n2Digit0, n1Digit0) - c, r1 = mulAdd64(n2Digit0, n1Digit1, c) - c, r2 = mulAdd64(n2Digit0, n1Digit2, c) - r4, r3 = mulAdd64(n2Digit0, n1Digit3, c) + c, _ = bits.Mul64(n2.n[0], n1.n[0]) + c, r1 = mulAdd64(n2.n[0], n1.n[1], c) + c, r2 = mulAdd64(n2.n[0], n1.n[2], c) + r4, r3 = mulAdd64(n2.n[0], n1.n[3], c) // Terms resulting from the product of the second digit of the second number // by all digits of the first number. // // Note that r1 is ignored because it is no longer needed to compute the // higher terms and it is shifted out below anyway. - c, _ = mulAdd64(n2Digit1, n1Digit0, r1) - c, r2 = mulAdd64Carry(n2Digit1, n1Digit1, r2, c) - c, r3 = mulAdd64Carry(n2Digit1, n1Digit2, r3, c) - r5, r4 = mulAdd64Carry(n2Digit1, n1Digit3, r4, c) + c, _ = mulAdd64(n2.n[1], n1.n[0], r1) + c, r2 = mulAdd64Carry(n2.n[1], n1.n[1], r2, c) + c, r3 = mulAdd64Carry(n2.n[1], n1.n[2], r3, c) + r5, r4 = mulAdd64Carry(n2.n[1], n1.n[3], r4, c) // Terms resulting from the product of the third digit of the second number // by all digits of the first number. // // Note that r2 is ignored because it is no longer needed to compute the // higher terms and it is shifted out below anyway. - c, _ = mulAdd64(n2Digit2, n1Digit0, r2) - c, r3 = mulAdd64Carry(n2Digit2, n1Digit1, r3, c) - c, r4 = mulAdd64Carry(n2Digit2, n1Digit2, r4, c) - r6, r5 = mulAdd64Carry(n2Digit2, n1Digit3, r5, c) + c, _ = mulAdd64(n2.n[2], n1.n[0], r2) + c, r3 = mulAdd64Carry(n2.n[2], n1.n[1], r3, c) + c, r4 = mulAdd64Carry(n2.n[2], n1.n[2], r4, c) + r6, r5 = mulAdd64Carry(n2.n[2], n1.n[3], r5, c) // Terms resulting from the product of the fourth digit of the second number // by all digits of the first number. // // Note that r3 is ignored because it is no longer needed to compute the // higher terms and it is shifted out below anyway. - c, _ = mulAdd64(n2Digit3, n1Digit0, r3) - c, r4 = mulAdd64Carry(n2Digit3, n1Digit1, r4, c) - c, r5 = mulAdd64Carry(n2Digit3, n1Digit2, r5, c) - r7, r6 = mulAdd64Carry(n2Digit3, n1Digit3, r6, c) + c, _ = mulAdd64(n2.n[3], n1.n[0], r3) + c, r4 = mulAdd64Carry(n2.n[3], n1.n[1], r4, c) + c, r5 = mulAdd64Carry(n2.n[3], n1.n[2], r5, c) + r7, r6 = mulAdd64Carry(n2.n[3], n1.n[3], r6, c) // At this point the upper 256 bits of the full 512-bit product n1*n2 are in // r4..r7 (recall the low order results were discarded as noted above). @@ -796,14 +786,10 @@ func mul512Rsh320Round(n1, n2 *ModNScalar) ModNScalar { // less than the group order given the group order is > 2^255 and the // maximum possible value of the result is 2^192. var result ModNScalar - result.n[0] = uint32(r0) - result.n[1] = uint32(r0 >> 32) - result.n[2] = uint32(r1) - result.n[3] = uint32(r1 >> 32) - result.n[4] = uint32(r2) - result.n[5] = uint32(r2 >> 32) - result.n[6] = uint32(r3) - result.n[7] = uint32(r3 >> 32) + result.n[0] = r0 + result.n[1] = r1 + result.n[2] = r2 + result.n[3] = r3 return result } diff --git a/dcrec/secp256k1/curve_test.go b/dcrec/secp256k1/curve_test.go index b1b4bec009..d3eb497f29 100644 --- a/dcrec/secp256k1/curve_test.go +++ b/dcrec/secp256k1/curve_test.go @@ -877,28 +877,16 @@ func TestScalarBaseMultJacobian(t *testing.T) { // modNBitLen returns the minimum number of bits required to represent the mod n // scalar. The result is 0 when the value is 0. func modNBitLen(s *ModNScalar) uint16 { - if w := s.n[7]; w > 0 { - return uint16(bits.Len32(w)) + 224 - } - if w := s.n[6]; w > 0 { - return uint16(bits.Len32(w)) + 192 - } - if w := s.n[5]; w > 0 { - return uint16(bits.Len32(w)) + 160 - } - if w := s.n[4]; w > 0 { - return uint16(bits.Len32(w)) + 128 - } if w := s.n[3]; w > 0 { - return uint16(bits.Len32(w)) + 96 + return uint16(bits.Len64(w)) + 192 } if w := s.n[2]; w > 0 { - return uint16(bits.Len32(w)) + 64 + return uint16(bits.Len64(w)) + 128 } if w := s.n[1]; w > 0 { - return uint16(bits.Len32(w)) + 32 + return uint16(bits.Len64(w)) + 64 } - return uint16(bits.Len32(s.n[0])) + return uint16(bits.Len64(s.n[0])) } // checkLambdaDecomposition returns an error if the provided decomposed scalars diff --git a/dcrec/secp256k1/field_test.go b/dcrec/secp256k1/field_test.go index a1200b8dd8..41a5a61757 100644 --- a/dcrec/secp256k1/field_test.go +++ b/dcrec/secp256k1/field_test.go @@ -1307,7 +1307,7 @@ func TestFieldMulInt(t *testing.T) { in2: 5, expected: "6cf4eb20f2447c77657fccb172d38c0aa91ea4ac446dc641fa463a6b5091fba7", }, { - name: "random sampling #3", + name: "random sampling #4", in1: "fb4529be3e027a3d1587d8a500b72f2d312e3577340ef5175f96d113be4c2ceb", in2: 8, expected: "da294df1f013d1e8ac3ec52805b979698971abb9a077a8bafcb688a4f261820f", @@ -1369,7 +1369,7 @@ func TestFieldMul(t *testing.T) { in2: "3", expected: "0", }, { - name: "secp256k1 prime * 3", + name: "secp256k1 prime * 8", in1: "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e", in2: "8", expected: "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc27", diff --git a/dcrec/secp256k1/modnscalar.go b/dcrec/secp256k1/modnscalar.go index 671260c3e0..8c3f87c74d 100644 --- a/dcrec/secp256k1/modnscalar.go +++ b/dcrec/secp256k1/modnscalar.go @@ -5,8 +5,10 @@ package secp256k1 import ( + "encoding/binary" "encoding/hex" "math/big" + "math/bits" "sync" ) @@ -18,81 +20,76 @@ import ( // https://cacr.uwaterloo.ca/hac/ // Many elliptic curve operations require working with scalars in a finite field -// characterized by the order of the group underlying the secp256k1 curve. -// Given this precision is larger than the biggest available native type, -// obviously some form of bignum math is needed. This code implements -// specialized fixed-precision field arithmetic rather than relying on an -// arbitrary-precision arithmetic package such as math/big for dealing with the -// math modulo the group order since the size is known. As a result, rather -// large performance gains are achieved by taking advantage of many -// optimizations not available to arbitrary-precision arithmetic and generic -// modular arithmetic algorithms. +// defined by the order of the group underlying the secp256k1 curve. Since the +// group order exceeds the width of the largest available native type, some form +// of multi-precision arithmetic is required. This code implements specialized +// fixed-precision field arithmetic rather than relying on an +// arbitrary-precision arithmetic package such as math/big for performing +// arithmetic modulo the fixed group order. As a result, significant +// performance gains are achieved by taking advantage of many optimizations not +// available to arbitrary-precision arithmetic and generic modular arithmetic +// algorithms. // -// There are various ways to internally represent each element. For example, -// the most obvious representation would be to use an array of 4 uint64s (64 -// bits * 4 = 256 bits). However, that representation suffers from the fact -// that there is no native Go type large enough to handle the intermediate -// results while adding or multiplying two 64-bit numbers. +// There are various ways to internally represent each element with their own +// pros and cons. Some common representations are: // -// Given the above, this implementation represents the field elements as 8 -// uint32s with each word (array entry) treated as base 2^32. This was chosen -// because most systems at the current time are 64-bit (or at least have 64-bit -// registers available for specialized purposes such as MMX) so the intermediate -// results can typically be done using a native register (and using uint64s to -// avoid the need for additional half-word arithmetic) - +// - 8 uint32s with base 2^32 limbs (aka 8x32: 32 bits * 8 = 256 bits) +// - 10 uint32s with base 2^26 limbs (aka 10x26: 26 bits * 10 = 260 bits) +// - 4 uint64s with base 2^64 limbs (aka 4x64: 64 bits * 4 = 256 bits) +// - 5 uint64s with base 2^52 limbs (aka 5x52: 52 bits * 5 = 260 bits) +// +// The primary tradeoff is complexity versus performance and the optimal choice +// also largely depends on the available hardware capabilities. +// +// For example, 5x52 and 10x26 allow performing several operations in a row +// before carry propagation or modular reduction becomes necessary since there +// are additional bits available in each limb, but that comes at the cost of +// manual magnitude tracking, multiple non-canonical representations of each +// number, and periodic normalization to propagate the accumulated carries and +// perform the modular reduction all at once. +// +// Conversely, 8x32 and 4x64 involve less complexity, are simpler to use, have +// fewer limbs to manage, and only have a single canonical representation for +// each number, but that comes at the cost of requiring carry propagation and +// modular reduction for every operation and larger intermediate results. +// +// The requirement for larger intermediate results is particularly notable for +// uint64s because there is no native Go type large enough to handle the 128-bit +// intermediate results while adding and multiplying the 64-bit limbs. +// +// This implementation historically used 8x32 for that reason, coupled with the +// fact that Go did not reliably make use of hardware-supported wide arithmetic, +// to ensure all intermediate results fit cleanly into uint64s. +// +// However, most systems are now 64-bit and almost all have instructions capable +// of handling the 128-bit intermediate results without the need for additional +// half-word arithmetic. Further, modern versions of Go automatically use those +// instructions on hardware that supports it. +// +// Given the above, this implementation represents the field elements as 4 +// uint64s with each limb (array entry) treated as base 2^64 (aka 4x64) and +// keeps the value reduced at all times. const ( - // These fields provide convenient access to each of the words of the + // These fields provide convenient access to each of the limbs of the // secp256k1 curve group order N to improve code readability. // // The group order of the curve per [SECG] is: - // 0xffffffff ffffffff ffffffff fffffffe baaedce6 af48a03b bfd25e8c d0364141 - // - // nolint: dupword - orderWordZero uint32 = 0xd0364141 - orderWordOne uint32 = 0xbfd25e8c - orderWordTwo uint32 = 0xaf48a03b - orderWordThree uint32 = 0xbaaedce6 - orderWordFour uint32 = 0xfffffffe - orderWordFive uint32 = 0xffffffff - orderWordSix uint32 = 0xffffffff - orderWordSeven uint32 = 0xffffffff - - // These fields provide convenient access to each of the words of the two's + // 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + orderLimb0 uint64 = 0xbfd25e8cd0364141 + orderLimb1 uint64 = 0xbaaedce6af48a03b + orderLimb2 uint64 = 0xfffffffffffffffe + orderLimb3 uint64 = 0xffffffffffffffff + + // These fields provide convenient access to each of the limbs of the two's // complement of the secp256k1 curve group order N to improve code // readability. // // The two's complement of the group order is: // 0x00000000 00000000 00000000 00000001 45512319 50b75fc4 402da173 2fc9bebf - orderComplementWordZero uint32 = (^orderWordZero) + 1 - orderComplementWordOne uint32 = ^orderWordOne - orderComplementWordTwo uint32 = ^orderWordTwo - orderComplementWordThree uint32 = ^orderWordThree - // orderComplementWordFour uint32 = ^orderWordFour // unused - // orderComplementWordFive uint32 = ^orderWordFive // unused - // orderComplementWordSix uint32 = ^orderWordSix // unused - // orderComplementWordSeven uint32 = ^orderWordSeven // unused - - // These fields provide convenient access to each of the words of the - // secp256k1 curve group order N / 2 to improve code readability and avoid - // the need to recalculate them. - // - // The half order of the secp256k1 curve group is: - // 0x7fffffff ffffffff ffffffff ffffffff 5d576e73 57a4501d dfe92f46 681b20a0 - // - // nolint: dupword - halfOrderWordZero uint32 = 0x681b20a0 - halfOrderWordOne uint32 = 0xdfe92f46 - halfOrderWordTwo uint32 = 0x57a4501d - halfOrderWordThree uint32 = 0x5d576e73 - halfOrderWordFour uint32 = 0xffffffff - halfOrderWordFive uint32 = 0xffffffff - halfOrderWordSix uint32 = 0xffffffff - halfOrderWordSeven uint32 = 0x7fffffff - - // uint32Mask is simply a mask with all bits set for a uint32 and is used to - // improve the readability of the code. - uint32Mask = 0xffffffff + orderComplementLimb0 uint64 = (^orderLimb0) + 1 + orderComplementLimb1 uint64 = ^orderLimb1 + orderComplementLimb2 uint64 = ^orderLimb2 + // orderComplementLimb3 uint64 = ^orderLimb3 // unused ) // ModNScalar implements optimized 256-bit constant-time fixed-precision @@ -106,39 +103,37 @@ const ( // around if absolutely needed. For example, subtraction can be performed by // adding the negation. // -// Should it be absolutely necessary, conversion to the standard library -// math/big.Int can be accomplished by using the Bytes method, slicing the -// resulting fixed-size array, and feeding it to big.Int.SetBytes. However, -// that should typically be avoided when possible as conversion to big.Ints -// requires allocations, is not constant time, and is slower when working modulo -// the group order. +// Should it be absolutely necessary, conversion to a standard library [big.Int] +// can be accomplished by using [ModNScalar.Bytes], slicing the resulting +// fixed-size array, and feeding it to [big.Int.SetBytes]. However, that should +// typically be avoided when possible as conversion to a [big.Int] requires +// allocations, is not constant time, and is slower when working modulo the +// group order. type ModNScalar struct { - // The scalar is represented as 8 32-bit integers in base 2^32. + // The scalar is represented as 4 64-bit integers in base 2^64. // // The following depicts the internal representation: - // --------------------------------------------------------- - // | n[7] | n[6] | ... | n[0] | - // | 32 bits | 32 bits | ... | 32 bits | - // | Mult: 2^(32*7) | Mult: 2^(32*6) | ... | Mult: 2^(32*0) | - // --------------------------------------------------------- + // -------------------------------------------------------------------- + // | n[3] | n[2] | n[1] | n[0] | + // | 64 bits | 64 bits | 64 bits | 64 bits | + // | Mult: 2^(64*3) | Mult: 2^(64*2) | Mult: 2^(64*1) | Mult: 2^(64*0) | + // -------------------------------------------------------------------- // - // For example, consider the number 2^87 + 2^42 + 1. It would be + // For example, consider the number 2^135 + 2^87 + 1. It would be // represented as: // n[0] = 1 - // n[1] = 2^10 - // n[2] = 2^23 - // n[3..7] = 0 - // - // The full 256-bit value is then calculated by looping i from 7..0 and - // doing sum(n[i] * 2^(32i)) like so: - // n[7] * 2^(32*7) = 0 * 2^224 = 0 - // n[6] * 2^(32*6) = 0 * 2^192 = 0 - // ... - // n[2] * 2^(32*2) = 2^23 * 2^64 = 2^87 - // n[1] * 2^(32*1) = 2^10 * 2^32 = 2^42 - // n[0] * 2^(32*0) = 1 * 2^0 = 1 - // Sum: 0 + 0 + ... + 2^87 + 2^42 + 1 = 2^87 + 2^42 + 1 - n [8]uint32 + // n[1] = 2^23 + // n[2] = 2^7 + // n[3] = 0 + // + // The full 256-bit value is then calculated by looping i from 3..0 and + // doing sum(n[i] * 2^(64i)) like so: + // n[3] * 2^(64*3) = 0 * 2^192 = 0 + // n[2] * 2^(64*2) = 2^7 * 2^128 = 2^135 + // n[1] * 2^(64*1) = 2^23 * 2^64 = 2^87 + // n[0] * 2^(64*0) = 1 * 2^0 = 1 + // Sum: 0 + 2^135 + 2^87 + 1 = 2^135 + 2^87 + 1 + n [4]uint64 } // String returns the scalar as a human-readable hex string. @@ -163,7 +158,7 @@ func (s *ModNScalar) Set(val *ModNScalar) *ModNScalar { // already set to zero. This function can be useful to clear an existing scalar // for reuse. func (s *ModNScalar) Zero() { - s.n = [8]uint32{} + s.n = [4]uint64{} } // IsZeroBit returns 1 when the scalar is equal to zero or 0 otherwise in @@ -174,16 +169,13 @@ func (s *ModNScalar) Zero() { // operations require a numeric value. See [ModNScalar.IsZero] for the version // that returns a bool. func (s *ModNScalar) IsZeroBit() uint32 { - // The scalar can only be zero if no bits are set in any of the words. - bits := s.n[0] | s.n[1] | s.n[2] | s.n[3] | s.n[4] | s.n[5] | s.n[6] | s.n[7] - return constantTimeEq(bits, 0) + return constantTimeEq64(s.n[0]|s.n[1]|s.n[2]|s.n[3], 0) } // IsZero returns whether or not the scalar is equal to zero in constant time. func (s *ModNScalar) IsZero() bool { // The scalar can only be zero if no bits are set in any of the words. - bits := s.n[0] | s.n[1] | s.n[2] | s.n[3] | s.n[4] | s.n[5] | s.n[6] | s.n[7] - return bits == 0 + return (s.n[0] | s.n[1] | s.n[2] | s.n[3]) == 0 } // SetInt sets the scalar to the passed integer in constant time. This is a @@ -193,79 +185,10 @@ func (s *ModNScalar) IsZero() bool { // The scalar is returned to support chaining. This enables syntax like: // s := new(ModNScalar).SetInt(2).Mul(s2) so that s = 2 * s2. func (s *ModNScalar) SetInt(ui uint32) *ModNScalar { - s.Zero() - s.n[0] = ui + s.n = [4]uint64{uint64(ui), 0, 0, 0} return s } -// overflows determines if the current scalar is greater than or equal to the -// group order in constant time and returns 1 if it is or 0 otherwise. -func (s *ModNScalar) overflows() uint32 { - // The intuition here is that the scalar is greater than the group order if - // one of the higher individual words is greater than corresponding word of - // the group order and all higher words in the scalar are equal to their - // corresponding word of the group order. Since this type is modulo the - // group order, being equal is also an overflow back to 0. - // - // Note that the words 5, 6, and 7 are all the max uint32 value, so there is - // no need to test if those individual words of the scalar exceeds them, - // hence, only equality is checked for them. - highWordsEqual := constantTimeEq(s.n[7], orderWordSeven) - highWordsEqual &= constantTimeEq(s.n[6], orderWordSix) - highWordsEqual &= constantTimeEq(s.n[5], orderWordFive) - overflow := highWordsEqual & constantTimeGreater(s.n[4], orderWordFour) - highWordsEqual &= constantTimeEq(s.n[4], orderWordFour) - overflow |= highWordsEqual & constantTimeGreater(s.n[3], orderWordThree) - highWordsEqual &= constantTimeEq(s.n[3], orderWordThree) - overflow |= highWordsEqual & constantTimeGreater(s.n[2], orderWordTwo) - highWordsEqual &= constantTimeEq(s.n[2], orderWordTwo) - overflow |= highWordsEqual & constantTimeGreater(s.n[1], orderWordOne) - highWordsEqual &= constantTimeEq(s.n[1], orderWordOne) - overflow |= highWordsEqual & constantTimeGreaterOrEq(s.n[0], orderWordZero) - - return overflow -} - -// reduce256 reduces the current scalar modulo the group order in accordance -// with the overflows parameter in constant time. The overflows parameter -// specifies whether or not the scalar is known to be greater than the group -// order and MUST either be 1 in the case it is or 0 in the case it is not for a -// correct result. -func (s *ModNScalar) reduce256(overflows uint32) { - // Notice that since s < 2^256 < 2N (where N is the group order), the max - // possible number of reductions required is one. Therefore, in the case a - // reduction is needed, it can be performed with a single subtraction of N. - // Also, recall that subtraction is equivalent to addition by the two's - // complement while ignoring the carry. - // - // When s >= N, the overflows parameter will be 1. Conversely, it will be 0 - // when s < N. Thus multiplying by the overflows parameter will either - // result in 0 or the multiplicand itself. - // - // Combining the above along with the fact that s + 0 = s, the following is - // a constant time implementation that works by either adding 0 or the two's - // complement of N as needed. - // - // The final result will be in the range 0 <= s < N as expected. - overflows64 := uint64(overflows) - c := uint64(s.n[0]) + overflows64*uint64(orderComplementWordZero) - s.n[0] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(s.n[1]) + overflows64*uint64(orderComplementWordOne) - s.n[1] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(s.n[2]) + overflows64*uint64(orderComplementWordTwo) - s.n[2] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(s.n[3]) + overflows64*uint64(orderComplementWordThree) - s.n[3] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(s.n[4]) + overflows64 // * 1 - s.n[4] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(s.n[5]) // + overflows64 * 0 - s.n[5] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(s.n[6]) // + overflows64 * 0 - s.n[6] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(s.n[7]) // + overflows64 * 0 - s.n[7] = uint32(c & uint32Mask) -} - // SetBytes interprets the provided array as a 256-bit big-endian unsigned // integer, reduces it modulo the group order, sets the scalar to the result, // and returns either 1 if it was reduced (aka it overflowed) or 0 otherwise in @@ -275,23 +198,41 @@ func (s *ModNScalar) reduce256(overflows uint32) { // from a bool to numeric value in constant time and many constant-time // operations require a numeric value. func (s *ModNScalar) SetBytes(b *[32]byte) uint32 { - // Pack the 256 total bits across the 8 uint32 words. This could be done + // Pack the 256 total bits across the 4 uint64 words. This could be done // with a for loop, but benchmarks show this unrolled version is about 2 // times faster than the variant that uses a loop. - s.n[0] = uint32(b[31]) | uint32(b[30])<<8 | uint32(b[29])<<16 | uint32(b[28])<<24 - s.n[1] = uint32(b[27]) | uint32(b[26])<<8 | uint32(b[25])<<16 | uint32(b[24])<<24 - s.n[2] = uint32(b[23]) | uint32(b[22])<<8 | uint32(b[21])<<16 | uint32(b[20])<<24 - s.n[3] = uint32(b[19]) | uint32(b[18])<<8 | uint32(b[17])<<16 | uint32(b[16])<<24 - s.n[4] = uint32(b[15]) | uint32(b[14])<<8 | uint32(b[13])<<16 | uint32(b[12])<<24 - s.n[5] = uint32(b[11]) | uint32(b[10])<<8 | uint32(b[9])<<16 | uint32(b[8])<<24 - s.n[6] = uint32(b[7]) | uint32(b[6])<<8 | uint32(b[5])<<16 | uint32(b[4])<<24 - s.n[7] = uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24 - - // The value might be >= N, so reduce it as required and return whether or - // not it was reduced. - needsReduce := s.overflows() - s.reduce256(needsReduce) - return needsReduce + s.n[0] = binary.BigEndian.Uint64(b[24:32]) + s.n[1] = binary.BigEndian.Uint64(b[16:24]) + s.n[2] = binary.BigEndian.Uint64(b[8:16]) + s.n[3] = binary.BigEndian.Uint64(b[0:8]) + + // Since s < 2^256 < 2N (where N is the secp256k1 group order), the max + // possible number of reductions required is one. Therefore, in the case a + // reduction is needed, it can be performed with a single subtraction of N. + // + // Since N must only conditionally be subtracted when s ≥ N, the following + // handles it in constant time by always calculating t = s - N and selecting + // the correct case via a constant time select. + + // Subtract N with borrow propagation. borrow is set iff s < N. + // + // In other words, the input overflowed (≥ N) when s - N does NOT borrow. + // + // t = s - N + var t0, t1, t2, t3, borrow uint64 + t0, borrow = bits.Sub64(s.n[0], orderLimb0, 0) + t1, borrow = bits.Sub64(s.n[1], orderLimb1, borrow) + t2, borrow = bits.Sub64(s.n[2], orderLimb2, borrow) + t3, borrow = bits.Sub64(s.n[3], orderLimb3, borrow) + + // Constant-time select. + // + // Set s = s when s < N (aka borrow is set). Otherwise s = t = s - N. + s.n[0] = constantTimeSelect64(borrow, s.n[0], t0) + s.n[1] = constantTimeSelect64(borrow, s.n[1], t1) + s.n[2] = constantTimeSelect64(borrow, s.n[2], t2) + s.n[3] = constantTimeSelect64(borrow, s.n[3], t3) + return uint32(1 - borrow) } // zeroArray32 zeroes the provided 32-byte buffer. @@ -311,6 +252,8 @@ func zeroArray32(b *[32]byte) { // size or it is acceptable to use this function with the described truncation // and overflow behavior. func (s *ModNScalar) SetByteSlice(b []byte) bool { + // Always copy a total of 32 bytes regardless of the input length to avoid + // introducing data-dependent timing. var b32 [32]byte b = b[:constantTimeMin(uint32(len(b)), 32)] copy(b32[:], b32[:32-len(b)]) @@ -324,49 +267,21 @@ func (s *ModNScalar) SetByteSlice(b []byte) bool { // into the passed byte slice in constant time. The target slice must have at // least 32 bytes available or it will panic. // -// There is a similar function, PutBytes, which unpacks the scalar into a -// 32-byte array directly. This version is provided since it can be useful to -// write directly into part of a larger buffer without needing a separate -// allocation. +// There is a similar function, [ModNScalar.PutBytes], which unpacks the scalar +// into a 32-byte array directly. This version is provided since it can be +// useful to write directly into part of a larger buffer without needing a +// separate allocation. // // Preconditions: // - The target slice MUST have at least 32 bytes available func (s *ModNScalar) PutBytesUnchecked(b []byte) { - // Unpack the 256 total bits from the 8 uint32 words. This could be done - // with a for loop, but benchmarks show this unrolled version is about 2 + // Unpack the 256 total bits from the 4 uint64 words. This could be done + // with a for loop, but benchmarks show this unrolled version is about 3 // times faster than the variant which uses a loop. - b[31] = byte(s.n[0]) - b[30] = byte(s.n[0] >> 8) - b[29] = byte(s.n[0] >> 16) - b[28] = byte(s.n[0] >> 24) - b[27] = byte(s.n[1]) - b[26] = byte(s.n[1] >> 8) - b[25] = byte(s.n[1] >> 16) - b[24] = byte(s.n[1] >> 24) - b[23] = byte(s.n[2]) - b[22] = byte(s.n[2] >> 8) - b[21] = byte(s.n[2] >> 16) - b[20] = byte(s.n[2] >> 24) - b[19] = byte(s.n[3]) - b[18] = byte(s.n[3] >> 8) - b[17] = byte(s.n[3] >> 16) - b[16] = byte(s.n[3] >> 24) - b[15] = byte(s.n[4]) - b[14] = byte(s.n[4] >> 8) - b[13] = byte(s.n[4] >> 16) - b[12] = byte(s.n[4] >> 24) - b[11] = byte(s.n[5]) - b[10] = byte(s.n[5] >> 8) - b[9] = byte(s.n[5] >> 16) - b[8] = byte(s.n[5] >> 24) - b[7] = byte(s.n[6]) - b[6] = byte(s.n[6] >> 8) - b[5] = byte(s.n[6] >> 16) - b[4] = byte(s.n[6] >> 24) - b[3] = byte(s.n[7]) - b[2] = byte(s.n[7] >> 8) - b[1] = byte(s.n[7] >> 16) - b[0] = byte(s.n[7] >> 24) + binary.BigEndian.PutUint64(b[0:8], s.n[3]) + binary.BigEndian.PutUint64(b[8:16], s.n[2]) + binary.BigEndian.PutUint64(b[16:24], s.n[1]) + binary.BigEndian.PutUint64(b[24:32], s.n[0]) } // PutBytes unpacks the scalar to a 32-byte big-endian value using the passed @@ -406,11 +321,8 @@ func (s *ModNScalar) IsOdd() bool { func (s *ModNScalar) Equals(val *ModNScalar) bool { // Xor only sets bits when they are different, so the two scalars can only // be the same if no bits are set after xoring each word. - bits := (s.n[0] ^ val.n[0]) | (s.n[1] ^ val.n[1]) | (s.n[2] ^ val.n[2]) | - (s.n[3] ^ val.n[3]) | (s.n[4] ^ val.n[4]) | (s.n[5] ^ val.n[5]) | - (s.n[6] ^ val.n[6]) | (s.n[7] ^ val.n[7]) - - return bits == 0 + return ((s.n[0] ^ val.n[0]) | (s.n[1] ^ val.n[1]) | (s.n[2] ^ val.n[2]) | + (s.n[3] ^ val.n[3])) == 0 } // Add2 adds the passed two scalars together modulo the group order in constant @@ -418,27 +330,42 @@ func (s *ModNScalar) Equals(val *ModNScalar) bool { // // The scalar is returned to support chaining. This enables syntax like: // s3.Add2(s, s2).AddInt(1) so that s3 = s + s2 + 1. -func (s *ModNScalar) Add2(val1, val2 *ModNScalar) *ModNScalar { - c := uint64(val1.n[0]) + uint64(val2.n[0]) - s.n[0] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(val1.n[1]) + uint64(val2.n[1]) - s.n[1] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(val1.n[2]) + uint64(val2.n[2]) - s.n[2] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(val1.n[3]) + uint64(val2.n[3]) - s.n[3] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(val1.n[4]) + uint64(val2.n[4]) - s.n[4] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(val1.n[5]) + uint64(val2.n[5]) - s.n[5] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(val1.n[6]) + uint64(val2.n[6]) - s.n[6] = uint32(c & uint32Mask) - c = (c >> 32) + uint64(val1.n[7]) + uint64(val2.n[7]) - s.n[7] = uint32(c & uint32Mask) - - // The result is now 256 bits, but it might still be >= N, so use the - // existing normal reduce method for 256-bit values. - s.reduce256(uint32(c>>32) + s.overflows()) +func (s *ModNScalar) Add2(a, b *ModNScalar) *ModNScalar { + // Since both values are already in the range 0 ≤ val < N (where N is the + // secp256k1 group order), the maximum possible result is < 2N - 1. So a + // maximum of one subtraction of N is required in the worst case. + // + // Since N must only conditionally be subtracted when a+b ≥ N, the following + // handles it in constant time by calculating both t = a+b and u = a+b - N + // and selecting the correct case via a constant time select. + + // Add with carry propagation. overflow is set iff t = a+b ≥ 2^256. + // + // t = a + b + var t0, t1, t2, t3, overflow, carry uint64 + t0, carry = bits.Add64(a.n[0], b.n[0], 0) + t1, carry = bits.Add64(a.n[1], b.n[1], carry) + t2, carry = bits.Add64(a.n[2], b.n[2], carry) + t3, overflow = bits.Add64(a.n[3], b.n[3], carry) + + // Subtract N with borrow propagation. borrow is set iff t = a+b < N. + // + // u = t - N = a+b - N + var u0, u1, u2, u3, borrow uint64 + u0, borrow = bits.Sub64(t0, orderLimb0, 0) + u1, borrow = bits.Sub64(t1, orderLimb1, borrow) + u2, borrow = bits.Sub64(t2, orderLimb2, borrow) + u3, borrow = bits.Sub64(t3, orderLimb3, borrow) + + // Constant-time select. + // + // Set s = t = a+b only when there was no overflow and t < N (borrow set). + // Otherwise s = u = a+b - N. + cond := (1 - overflow) & borrow + s.n[0] = constantTimeSelect64(cond, t0, u0) + s.n[1] = constantTimeSelect64(cond, t1, u1) + s.n[2] = constantTimeSelect64(cond, t2, u2) + s.n[3] = constantTimeSelect64(cond, t3, u3) return s } @@ -451,315 +378,359 @@ func (s *ModNScalar) Add(val *ModNScalar) *ModNScalar { return s.Add2(s, val) } -// accumulator96 provides a 96-bit accumulator for use in the intermediate -// calculations requiring more than 64-bits. -type accumulator96 struct { - n [3]uint32 -} +// scalar64Reduce512 reduces a 512-bit little-endian limb array modulo the group +// order in constant time and stores the result in r. +func scalar64Reduce512(r *[4]uint64, x *[8]uint64) { + // The overall strategy employed here is: + // 1) Start with the full unreduced 512-bit product of the two scalars. + // 2) Reduce the result modulo the group order via Crandall reduction + // (described below). + // 3) Repeat step 2 noting that each iteration reduces the required number + // of bits by 127 because the two's complement of N has 127 leading zero + // bits. + // 4) Once reduced to less than a max of 2N, perform the final reduction + // with a constant-time conditional subtraction. + // + // Technically the max possible input value here will be (N-1)^2, since the + // only time it is called is with the product of two scalars that are always + // mod N. Nevertheless, it is safer to treat it as the product of two max + // size 256-bit values, (2^256-1)^2 = 2^512 - 2^257 + 1, to ensure + // correctness if that were to ever change. + // + // Per [HAC] section 14.3.4: Reduction method of moduli of special form, + // when the modulus is of the special form m = b^t - c, highly efficient + // reduction can be achieved. While [HAC] only presents the algorithm and + // does not call it out by name or provide the mathematical justification, + // the underlying technique is known as Crandall reduction and is often + // presented as 2^k - c. It is easy to see they are equivalent by setting + // b = 2 and t = k. + // + // The secp256k1 group order is 2^256 - 0x14551231950b75fc4402da1732fc9bebf, + // so it fits this criteria where: + // k = 256 + // c = 432420386565659656852420866394968145599 + // + // Crandall reduction works by taking advantage of the fact that if a prime + // is of the form 2^k - c, then 2^k - c ≡ 0 (mod p), so 2^k ≡ c (mod p). In + // other words, every multiple of 2^k is equivalent to adding c when working + // modulo p. + // + // Since the 512-bit value to reduce is tightly packed into uint64s, the + // upper 4 limbs are all multiples of 2^256. Therefore, reducing modulo the + // group order is equivalent to multiplying those upper limbs by c and + // adding the result to the corresponding lower 4 limbs while propagating + // the carries. + // + // For the specific case of the secp256k1 group order, a max of 4 reductions + // are required because c is 129 bits and so the first round will reduce + // from 512 bits to a max of 385 bits, the second round will reduce to a max + // of 258 bits, and the third round will reduce to within 2N. Then, a + // conditional subtraction of N handles the final reduction. + // + // The reduction is slightly complicated by the fact c needs 3 uint64 limbs + // to represent it, so multiplying the upper limbs by c requires multiplying + // each one by all 3 limbs of c and carefully managing the columns and + // carries. The uppermost limb of c = 1, so multiplication by that limb is + // avoided in the code below. -// Add adds the passed unsigned 64-bit value to the accumulator. -func (a *accumulator96) Add(v uint64) { - low := uint32(v & uint32Mask) - hi := uint32(v >> 32) - a.n[0] += low - hi += constantTimeLess(a.n[0], low) // Carry if overflow in n[0]. - a.n[1] += hi - a.n[2] += constantTimeLess(a.n[1], hi) // Carry if overflow in n[1]. -} + var t0, t1, t2, t3, t4, t5, t6, l0, h0, l1, h1, l2, carry uint64 -// Rsh32 right shifts the accumulator by 32 bits. -func (a *accumulator96) Rsh32() { - a.n[0] = a.n[1] - a.n[1] = a.n[2] - a.n[2] = 0 -} + // ------------------------------------------------------------------------- + // First reduction. + // + // The code in this section is equivalent to the following if uint512 and + // larger numbers and shifts were supported directly: + // + // t0 = uint64(x%(1<<256) + (x>>256)*c) + // t1 = uint64((x%(1<<256) + (x>>256)*c) >> 64) + // t2 = uint64((x%(1<<256) + (x>>256)*c) >> 128) + // t3 = uint64((x%(1<<256) + (x>>256)*c) >> 192) + // t4 = uint64((x%(1<<256) + (x>>256)*c) >> 256) + // t5 = uint64((x%(1<<256) + (x>>256)*c) >> 320) + // t6 = uint64((x%(1<<256) + (x>>256)*c) >> 384) + // + // To visualize the logic, multiply the 3 limbs of c by the upper limbs via + // the standard pencil-and-paper method: + // + // 2^576 2^512 2^448 2^384 2^320 2^256 (place values) + // ----------------------------------- + // x7 x6 x5 x4 + // c2 c1 c0 * + // ----------------------------------- + // x4c2 x4c1 x4c0 + // x5c2 x5c1 x5c0 + + // x6c2 x6c1 x6c0 + + // x7c2 x7c1 x7c0 + + // ----------------------------------- + // x7c2 .... .... .... .... x4c0 + // + // Then add them to the associated lower limbs per the reduction identity: + // + // 2^384 2^320 2^256 2^192 2^128 2^64 2^0 (place values) + // ----------------------------------------- + // x3 x2 x1 x0 + // x4c2 x4c1 x4c0 + + // x5c2 x5c1 x5c0 + + // x6c2 x6c1 x6c0 + + // x7c2 x7c1 x7c0 + + // ----------------------------------------- + // t6 t5 t4 t3 t2 t1 t0 + // + // Where t6 ≤ 1 for the potential carry. + // + // Thus 0 ≤ t < 2^385 after the 1st reduction. + // ------------------------------------------------------------------------- + + // Compute x4*c with result in t0..t2 with carries propagated and the final + // carry in t3. + h0, t0 = bits.Mul64(x[4], orderComplementLimb0) + h1, t1 = bits.Mul64(x[4], orderComplementLimb1) + t1, carry = bits.Add64(t1, h0, 0) + t2, carry = bits.Add64(x[4], h1, carry) + t3 = carry + + // Compute x5*c with result added to t1..t3 with carries propagated and the + // final carry in t4. + // + // Note that since h1 is the upper 64 bits of the product of a uint64 with + // c1 (limb 1 of c) and c1 < 2^63 - 1: + // h1 ≤ floor((2^64-1)(2^63-1) / 2^64) = 2^63 - 2 + // + // Then, because current t3 ≤ 1 and carry ≤ 1, a loose bound for the first + // t3 below is: + // t3 ≤ h1 + 2 = 2^63 < 2^64 + // + // Therefore, it is safe to discard the carry. + h0, l0 = bits.Mul64(x[5], orderComplementLimb0) + h1, l1 = bits.Mul64(x[5], orderComplementLimb1) + t1, carry = bits.Add64(t1, l0, 0) + t2, carry = bits.Add64(t2, h0, carry) + t3, _ = bits.Add64(t3, h1, carry) + t2, carry = bits.Add64(t2, l1, 0) + t3, carry = bits.Add64(t3, x[5], carry) + t4 = carry + + // Compute x6*c with result added to t2..t4 with carries propagated and the + // final carry in t5. + // + // It is safe to discard the carry on the first t4 for the same reason as + // above. + h0, l0 = bits.Mul64(x[6], orderComplementLimb0) + h1, l1 = bits.Mul64(x[6], orderComplementLimb1) + t2, carry = bits.Add64(t2, l0, 0) + t3, carry = bits.Add64(t3, h0, carry) + t4, _ = bits.Add64(t4, h1, carry) + t3, carry = bits.Add64(t3, l1, 0) + t4, carry = bits.Add64(t4, x[6], carry) + t5 = carry + + // Compute x7*c with result added to t3..t5 with carries propagated and the + // final carry in t6. + // + // It is safe to discard the carry on the first t5 for the same reason as + // above. + h0, l0 = bits.Mul64(x[7], orderComplementLimb0) + h1, l1 = bits.Mul64(x[7], orderComplementLimb1) + t3, carry = bits.Add64(t3, l0, 0) + t4, carry = bits.Add64(t4, h0, carry) + t5, _ = bits.Add64(t5, h1, carry) + t4, carry = bits.Add64(t4, l1, 0) + t5, carry = bits.Add64(t5, x[7], carry) + t6 = carry + + // Add result to lower limbs and propagate carries. + // + // It is safe to discard the carry on t6 since the resulting t6 ≤ 1 as + // previously described. + t0, carry = bits.Add64(t0, x[0], 0) + t1, carry = bits.Add64(t1, x[1], carry) + t2, carry = bits.Add64(t2, x[2], carry) + t3, carry = bits.Add64(t3, x[3], carry) + t4, carry = bits.Add64(t4, 0, carry) + t5, carry = bits.Add64(t5, 0, carry) + t6, _ = bits.Add64(t6, 0, carry) + + // ------------------------------------------------------------------------- + // Second reduction. + // + // The value now fits in 385 bits, so reduce it again. Only t4, t5, and t6 + // need to be considered since the higher limb, t7, is ≥ 448 bits and thus + // guaranteed to be 0. Further, as previously noted, t6 ≤ 1. + // + // The code in this section is equivalent to following if uint512 and larger + // numbers and shifts were supported: + // + // t0 = uint64(t%2^256 + (t>>256)*c) + // t1 = uint64((t%2^256 + (t>>256)*c) >> 64) + // t2 = uint64((t%2^256 + (t>>256)*c) >> 128) + // t3 = uint64((t%2^256 + (t>>256)*c) >> 192) + // t4 = uint64((t%2^256 + (t>>256)*c) >> 256) + // + // To visualize the logic, multiply the 3 limbs of c by the new upper limbs + // via the standard pencil-and-paper method: + // + // 2^512 2^448 2^384 2^320 2^256 (place values) + // ----------------------------- + // t6 t5 t4 + // c2 c1 c0 * + // ----------------------------- + // t4c2 t4c1 t4c0 + // t5c2 t5c1 t5c0 + + // t6c2 t6c1 t6c0 + + // ---------------------------- + // t6c2 .... .... .... t4c0 + // + // Then add them to the associated lower limbs per the reduction identity + // noting that the code reuses t for the sums: + // + // 2^256 2^192 2^128 2^64 2^0 (place values) + // ----------------------------- + // t3 t2 t1 t0 + // t4c2 t4c1 t4c0 + + // t5c2 t5c1 t5c0 + + // t6c2 t6c1 t6c0 + + // ----------------------------- + // t4 t3 t2 t1 t0 + // + // With a loose bound for t4 ≤ 3. + // + // Proof: + // + // Known: c2=1, t6 ≤ 1, t3 ≤ 2^64-1, t5 ≤ 2^64-1, and c1 ≤ 2^63-1. + // + // Let s = t3 + t5c2 + t6c1. Then: + // s ≤ 2^64-1 + (2^64-1)*1 + 1*(2^63-1) = 2^65 + 2^63 - 3 + // + // So, the loose bound on the carry in to t4 is carryIn ≤ floor(s/2^64) = 2. + // + // Finally, t4 ≤ t6*c2 + carryIn ≤ 1*1 + 2 ≤ 3. + // + // Thus 0 ≤ t < 2^258 after the 2nd reduction. + // ------------------------------------------------------------------------- -// reduce385 reduces the 385-bit intermediate result in the passed terms modulo -// the group order in constant time and stores the result in s. -func (s *ModNScalar) reduce385(t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12 uint64) { - // At this point, the intermediate result in the passed terms has been - // reduced to fit within 385 bits, so reduce it again using the same method - // described in reduce512. As before, the intermediate result will end up - // being reduced by another 127 bits to 258 bits, thus 9 32-bit terms are - // needed for this iteration. The reduced terms are assigned back to t0 - // through t8. - // - // Note that several of the intermediate calculations require adding 64-bit - // products together which would overflow a uint64, so a 96-bit accumulator - // is used instead until the value is reduced enough to use native uint64s. - - // Terms for 2^(32*0). - var acc accumulator96 - acc.n[0] = uint32(t0) // == acc.Add(t0) because acc is guaranteed to be 0. - acc.Add(t8 * uint64(orderComplementWordZero)) - t0 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*1). - acc.Add(t1) - acc.Add(t8 * uint64(orderComplementWordOne)) - acc.Add(t9 * uint64(orderComplementWordZero)) - t1 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*2). - acc.Add(t2) - acc.Add(t8 * uint64(orderComplementWordTwo)) - acc.Add(t9 * uint64(orderComplementWordOne)) - acc.Add(t10 * uint64(orderComplementWordZero)) - t2 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*3). - acc.Add(t3) - acc.Add(t8 * uint64(orderComplementWordThree)) - acc.Add(t9 * uint64(orderComplementWordTwo)) - acc.Add(t10 * uint64(orderComplementWordOne)) - acc.Add(t11 * uint64(orderComplementWordZero)) - t3 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*4). - acc.Add(t4) - acc.Add(t8) // * uint64(orderComplementWordFour) // * 1 - acc.Add(t9 * uint64(orderComplementWordThree)) - acc.Add(t10 * uint64(orderComplementWordTwo)) - acc.Add(t11 * uint64(orderComplementWordOne)) - acc.Add(t12 * uint64(orderComplementWordZero)) - t4 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*5). - acc.Add(t5) - // acc.Add(t8 * uint64(orderComplementWordFive)) // 0 - acc.Add(t9) // * uint64(orderComplementWordFour) // * 1 - acc.Add(t10 * uint64(orderComplementWordThree)) - acc.Add(t11 * uint64(orderComplementWordTwo)) - acc.Add(t12 * uint64(orderComplementWordOne)) - t5 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*6). - acc.Add(t6) - // acc.Add(t8 * uint64(orderComplementWordSix)) // 0 - // acc.Add(t9 * uint64(orderComplementWordFive)) // 0 - acc.Add(t10) // * uint64(orderComplementWordFour) // * 1 - acc.Add(t11 * uint64(orderComplementWordThree)) - acc.Add(t12 * uint64(orderComplementWordTwo)) - t6 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*7). - acc.Add(t7) - // acc.Add(t8 * uint64(orderComplementWordSeven)) // 0 - // acc.Add(t9 * uint64(orderComplementWordSix)) // 0 - // acc.Add(t10 * uint64(orderComplementWordFive)) // 0 - acc.Add(t11) // * uint64(orderComplementWordFour) // * 1 - acc.Add(t12 * uint64(orderComplementWordThree)) - t7 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*8). - // acc.Add(t9 * uint64(orderComplementWordSeven)) // 0 - // acc.Add(t10 * uint64(orderComplementWordSix)) // 0 - // acc.Add(t11 * uint64(orderComplementWordFive)) // 0 - acc.Add(t12) // * uint64(orderComplementWordFour) // * 1 - t8 = uint64(acc.n[0]) - // acc.Rsh32() // No need since not used after this. Guaranteed to be 0. - - // NOTE: All of the remaining multiplications for this iteration result in 0 - // as they all involve multiplying by combinations of the fifth, sixth, and - // seventh words of the two's complement of N, which are 0, so skip them. - - // At this point, the result is reduced to fit within 258 bits, so reduce it - // again using a slightly modified version of the same method. The maximum - // value in t8 is 2 at this point and therefore multiplying it by each word - // of the two's complement of N and adding it to a 32-bit term will result - // in a maximum requirement of 33 bits, so it is safe to use native uint64s - // here for the intermediate term carry propagation. - // - // Also, since the maximum value in t8 is 2, this ends up reducing by - // another 2 bits to 256 bits. - c := t0 + t8*uint64(orderComplementWordZero) - s.n[0] = uint32(c & uint32Mask) - c = (c >> 32) + t1 + t8*uint64(orderComplementWordOne) - s.n[1] = uint32(c & uint32Mask) - c = (c >> 32) + t2 + t8*uint64(orderComplementWordTwo) - s.n[2] = uint32(c & uint32Mask) - c = (c >> 32) + t3 + t8*uint64(orderComplementWordThree) - s.n[3] = uint32(c & uint32Mask) - c = (c >> 32) + t4 + t8 // * uint64(orderComplementWordFour) == * 1 - s.n[4] = uint32(c & uint32Mask) - c = (c >> 32) + t5 // + t8*uint64(orderComplementWordFive) == 0 - s.n[5] = uint32(c & uint32Mask) - c = (c >> 32) + t6 // + t8*uint64(orderComplementWordSix) == 0 - s.n[6] = uint32(c & uint32Mask) - c = (c >> 32) + t7 // + t8*uint64(orderComplementWordSeven) == 0 - s.n[7] = uint32(c & uint32Mask) - - // The result is now 256 bits, but it might still be >= N, so use the - // existing normal reduce method for 256-bit values. - s.reduce256(uint32(c>>32) + s.overflows()) -} + // Compute t4*c with result added to t0..t2 with carries propagated and the + // final carry in t4. + // + // It is safe to discard the carry on the final t4 given the carries ≤ 1. + h0, l0 = bits.Mul64(t4, orderComplementLimb0) + h1, l1 = bits.Mul64(t4, orderComplementLimb1) + l2 = t4 // h2, l2 = bits.Mul64(t4, orderComplementLimb2) => h2=0, l2=t4 + t0, carry = bits.Add64(t0, l0, 0) + t1, carry = bits.Add64(t1, h0, carry) + t2, carry = bits.Add64(t2, h1, carry) + t3, carry = bits.Add64(t3, 0, carry) + t4 = carry + t1, carry = bits.Add64(t1, l1, 0) + t2, carry = bits.Add64(t2, l2, carry) + t3, carry = bits.Add64(t3, 0, carry) + t4, _ = bits.Add64(t4, 0, carry) + + // Compute t5*c with result added to t1..t3 with carries propagated. + // + // It is safe to discard the carry on the final t4 given the carries ≤ 1. + h0, l0 = bits.Mul64(t5, orderComplementLimb0) + h1, l1 = bits.Mul64(t5, orderComplementLimb1) + // h2, l2 = bits.Mul64(t5, orderComplementLimb2) => h2=0, l2=t5 + t1, carry = bits.Add64(t1, l0, 0) + t2, carry = bits.Add64(t2, h0, carry) + t3, carry = bits.Add64(t3, h1, carry) + t4, _ = bits.Add64(t4, 0, carry) + t2, carry = bits.Add64(t2, l1, 0) + t3, carry = bits.Add64(t3, t5, carry) + t4, _ = bits.Add64(t4, 0, carry) + + // Compute t6*c with result added to t2..t4 with carries propagated. + // + // Note that since t6 ≤ 1, the product can't overflow a uint64, so it is + // safe to use normal 64-bit multiplication. + // + // It is safe to discard the carry on t4 since the resulting t4 ≤ 3 as + // previously described. + t2, carry = bits.Add64(t2, t6*orderComplementLimb0, 0) + t3, carry = bits.Add64(t3, t6*orderComplementLimb1, carry) + t4, _ = bits.Add64(t4, t6, carry) + + // ------------------------------------------------------------------------- + // Third reduction. + // + // The value now fits in 258 bits, so reduce it again. Only t4 needs to be + // considered since the higher limbs are ≥ 320 bits and thus guaranteed to + // be 0. + // + // The code in this section is equivalent to following if uint512 and larger + // numbers and shifts were supported: + // + // t0 = uint64(t%2^256 + (t>>256)*c) + // t1 = uint64((t%2^256 + (t>>256)*c) >> 64) + // t2 = uint64((t%2^256 + (t>>256)*c) >> 128) + // t3 = uint64((t%2^256 + (t>>256)*c) >> 192) + // t4 = uint64((t%2^256 + (t>>256)*c) >> 256) + // + // To visualize the logic, multiply the 3 limbs of c by the new upper limb + // via the standard pencil-and-paper method: + // + // 2^512 2^448 2^384 2^320 2^256 (place values) + // ----------------------------- + // t4 + // c2 c1 c0 * + // ----------------------------- + // t4c2 t4c1 t4c0 + // + // Then add them to the associated lower limbs noting that the code reuses t + // for the sums: + // + // 2^256 2^192 2^128 2^64 2^0 (place values) + // ----------------------------- + // t3 t2 t1 t0 + // t4c2 t4c1 t4c0 + + // ----------------------------- + // t4 t3 t2 t1 t0 + // + // Where t4 ≤ 1 for the carry. + // ------------------------------------------------------------------------- -// reduce512 reduces the 512-bit intermediate result in the passed terms modulo -// the group order down to 385 bits in constant time and stores the result in s. -func (s *ModNScalar) reduce512(t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15 uint64) { - // At this point, the intermediate result in the passed terms is grouped - // into the respective bases. + // Compute t4*c with result added to t0..t2 with carries propagated and the + // final carry in t4. // - // Per [HAC] section 14.3.4: Reduction method of moduli of special form, - // when the modulus is of the special form m = b^t - c, where log_2(c) < t, - // highly efficient reduction can be achieved per the provided algorithm. - // - // The secp256k1 group order fits this criteria since it is: - // 2^256 - 432420386565659656852420866394968145599 - // - // Technically the max possible value here is (N-1)^2 since the two scalars - // being multiplied are always mod N. Nevertheless, it is safer to consider - // it to be (2^256-1)^2 = 2^512 - 2^257 + 1 since it is the product of two - // 256-bit values. - // - // The algorithm is to reduce the result modulo the prime by subtracting - // multiples of the group order N. However, in order simplify carry - // propagation, this adds with the two's complement of N to achieve the same - // result. - // - // Since the two's complement of N has 127 leading zero bits, this will end - // up reducing the intermediate result from 512 bits to 385 bits, resulting - // in 13 32-bit terms. The reduced terms are assigned back to t0 through - // t12. - // - // Note that several of the intermediate calculations require adding 64-bit - // products together which would overflow a uint64, so a 96-bit accumulator - // is used instead. - - // Terms for 2^(32*0). - var acc accumulator96 - acc.n[0] = uint32(t0) // == acc.Add(t0) because acc is guaranteed to be 0. - acc.Add(t8 * uint64(orderComplementWordZero)) - t0 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*1). - acc.Add(t1) - acc.Add(t8 * uint64(orderComplementWordOne)) - acc.Add(t9 * uint64(orderComplementWordZero)) - t1 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*2). - acc.Add(t2) - acc.Add(t8 * uint64(orderComplementWordTwo)) - acc.Add(t9 * uint64(orderComplementWordOne)) - acc.Add(t10 * uint64(orderComplementWordZero)) - t2 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*3). - acc.Add(t3) - acc.Add(t8 * uint64(orderComplementWordThree)) - acc.Add(t9 * uint64(orderComplementWordTwo)) - acc.Add(t10 * uint64(orderComplementWordOne)) - acc.Add(t11 * uint64(orderComplementWordZero)) - t3 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*4). - acc.Add(t4) - acc.Add(t8) // * uint64(orderComplementWordFour) // * 1 - acc.Add(t9 * uint64(orderComplementWordThree)) - acc.Add(t10 * uint64(orderComplementWordTwo)) - acc.Add(t11 * uint64(orderComplementWordOne)) - acc.Add(t12 * uint64(orderComplementWordZero)) - t4 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*5). - acc.Add(t5) - // acc.Add(t8 * uint64(orderComplementWordFive)) // 0 - acc.Add(t9) // * uint64(orderComplementWordFour) // * 1 - acc.Add(t10 * uint64(orderComplementWordThree)) - acc.Add(t11 * uint64(orderComplementWordTwo)) - acc.Add(t12 * uint64(orderComplementWordOne)) - acc.Add(t13 * uint64(orderComplementWordZero)) - t5 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*6). - acc.Add(t6) - // acc.Add(t8 * uint64(orderComplementWordSix)) // 0 - // acc.Add(t9 * uint64(orderComplementWordFive)) // 0 - acc.Add(t10) // * uint64(orderComplementWordFour)) // * 1 - acc.Add(t11 * uint64(orderComplementWordThree)) - acc.Add(t12 * uint64(orderComplementWordTwo)) - acc.Add(t13 * uint64(orderComplementWordOne)) - acc.Add(t14 * uint64(orderComplementWordZero)) - t6 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*7). - acc.Add(t7) - // acc.Add(t8 * uint64(orderComplementWordSeven)) // 0 - // acc.Add(t9 * uint64(orderComplementWordSix)) // 0 - // acc.Add(t10 * uint64(orderComplementWordFive)) // 0 - acc.Add(t11) // * uint64(orderComplementWordFour) // * 1 - acc.Add(t12 * uint64(orderComplementWordThree)) - acc.Add(t13 * uint64(orderComplementWordTwo)) - acc.Add(t14 * uint64(orderComplementWordOne)) - acc.Add(t15 * uint64(orderComplementWordZero)) - t7 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*8). - // acc.Add(t9 * uint64(orderComplementWordSeven)) // 0 - // acc.Add(t10 * uint64(orderComplementWordSix)) // 0 - // acc.Add(t11 * uint64(orderComplementWordFive)) // 0 - acc.Add(t12) // * uint64(orderComplementWordFour) // * 1 - acc.Add(t13 * uint64(orderComplementWordThree)) - acc.Add(t14 * uint64(orderComplementWordTwo)) - acc.Add(t15 * uint64(orderComplementWordOne)) - t8 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*9). - // acc.Add(t10 * uint64(orderComplementWordSeven)) // 0 - // acc.Add(t11 * uint64(orderComplementWordSix)) // 0 - // acc.Add(t12 * uint64(orderComplementWordFive)) // 0 - acc.Add(t13) // * uint64(orderComplementWordFour) // * 1 - acc.Add(t14 * uint64(orderComplementWordThree)) - acc.Add(t15 * uint64(orderComplementWordTwo)) - t9 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*10). - // acc.Add(t11 * uint64(orderComplementWordSeven)) // 0 - // acc.Add(t12 * uint64(orderComplementWordSix)) // 0 - // acc.Add(t13 * uint64(orderComplementWordFive)) // 0 - acc.Add(t14) // * uint64(orderComplementWordFour) // * 1 - acc.Add(t15 * uint64(orderComplementWordThree)) - t10 = uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*11). - // acc.Add(t12 * uint64(orderComplementWordSeven)) // 0 - // acc.Add(t13 * uint64(orderComplementWordSix)) // 0 - // acc.Add(t14 * uint64(orderComplementWordFive)) // 0 - acc.Add(t15) // * uint64(orderComplementWordFour) // * 1 - t11 = uint64(acc.n[0]) - acc.Rsh32() - - // NOTE: All of the remaining multiplications for this iteration result in 0 - // as they all involve multiplying by combinations of the fifth, sixth, and - // seventh words of the two's complement of N, which are 0, so skip them. - - // Terms for 2^(32*12). - t12 = uint64(acc.n[0]) - // acc.Rsh32() // No need since not used after this. Guaranteed to be 0. - - // At this point, the result is reduced to fit within 385 bits, so reduce it - // again using the same method accordingly. - s.reduce385(t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12) + // Note that since current t4 ≤ 3, these bounds for the product hold: + // t4*c0 < 2^64 + // t4*c1 < 2^64 + // + // So, uint64 overflow is impossible and therefore it is safe to use normal + // 64-bit multiplication. + t0, carry = bits.Add64(t0, t4*orderComplementLimb0, 0) + t1, carry = bits.Add64(t1, t4*orderComplementLimb1, carry) + t2, carry = bits.Add64(t2, t4, carry) + t3, carry = bits.Add64(t3, 0, carry) + t4 = carry + + // ------------------------------------------------------------------------- + // Final reduction. + // + // The value is now in the range 0 ≤ t < 2N, so one 5-limb conditional + // subtract of N guarantees it is fully reduced. + // ------------------------------------------------------------------------- + + // Subtract N with borrow propagation. borrow is set iff t < N. + // + // In other words, the value overflowed (≥ N) when t - N does NOT borrow. + // + // s = t - N + var s0, s1, s2, s3, borrow uint64 + s0, borrow = bits.Sub64(t0, orderLimb0, 0) + s1, borrow = bits.Sub64(t1, orderLimb1, borrow) + s2, borrow = bits.Sub64(t2, orderLimb2, borrow) + s3, borrow = bits.Sub64(t3, orderLimb3, borrow) + _, borrow = bits.Sub64(t4, 0, borrow) + + // Constant-time select. + // + // Set r = t only when t < N (borrow set). + // Otherwise r = s = t - N. + r[0] = constantTimeSelect64(borrow, t0, s0) + r[1] = constantTimeSelect64(borrow, t1, s1) + r[2] = constantTimeSelect64(borrow, t2, s2) + r[3] = constantTimeSelect64(borrow, t3, s3) } // Mul2 multiplies the passed two scalars together modulo the group order in @@ -767,159 +738,10 @@ func (s *ModNScalar) reduce512(t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, // // The scalar is returned to support chaining. This enables syntax like: // s3.Mul2(s, s2).AddInt(1) so that s3 = (s * s2) + 1. -func (s *ModNScalar) Mul2(val, val2 *ModNScalar) *ModNScalar { - // This could be done with for loops and an array to store the intermediate - // terms, but this unrolled version is significantly faster. - - // The overall strategy employed here is: - // 1) Calculate the 512-bit product of the two scalars using the standard - // pencil-and-paper method. - // 2) Reduce the result modulo the prime by effectively subtracting - // multiples of the group order N (actually performed by adding multiples - // of the two's complement of N to avoid implementing subtraction). - // 3) Repeat step 2 noting that each iteration reduces the required number - // of bits by 127 because the two's complement of N has 127 leading zero - // bits. - // 4) Once reduced to 256 bits, call the existing reduce method to perform - // a final reduction as needed. - // - // Note that several of the intermediate calculations require adding 64-bit - // products together which would overflow a uint64, so a 96-bit accumulator - // is used instead. - - // Terms for 2^(32*0). - var acc accumulator96 - acc.Add(uint64(val.n[0]) * uint64(val2.n[0])) - t0 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*1). - acc.Add(uint64(val.n[0]) * uint64(val2.n[1])) - acc.Add(uint64(val.n[1]) * uint64(val2.n[0])) - t1 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*2). - acc.Add(uint64(val.n[0]) * uint64(val2.n[2])) - acc.Add(uint64(val.n[1]) * uint64(val2.n[1])) - acc.Add(uint64(val.n[2]) * uint64(val2.n[0])) - t2 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*3). - acc.Add(uint64(val.n[0]) * uint64(val2.n[3])) - acc.Add(uint64(val.n[1]) * uint64(val2.n[2])) - acc.Add(uint64(val.n[2]) * uint64(val2.n[1])) - acc.Add(uint64(val.n[3]) * uint64(val2.n[0])) - t3 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*4). - acc.Add(uint64(val.n[0]) * uint64(val2.n[4])) - acc.Add(uint64(val.n[1]) * uint64(val2.n[3])) - acc.Add(uint64(val.n[2]) * uint64(val2.n[2])) - acc.Add(uint64(val.n[3]) * uint64(val2.n[1])) - acc.Add(uint64(val.n[4]) * uint64(val2.n[0])) - t4 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*5). - acc.Add(uint64(val.n[0]) * uint64(val2.n[5])) - acc.Add(uint64(val.n[1]) * uint64(val2.n[4])) - acc.Add(uint64(val.n[2]) * uint64(val2.n[3])) - acc.Add(uint64(val.n[3]) * uint64(val2.n[2])) - acc.Add(uint64(val.n[4]) * uint64(val2.n[1])) - acc.Add(uint64(val.n[5]) * uint64(val2.n[0])) - t5 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*6). - acc.Add(uint64(val.n[0]) * uint64(val2.n[6])) - acc.Add(uint64(val.n[1]) * uint64(val2.n[5])) - acc.Add(uint64(val.n[2]) * uint64(val2.n[4])) - acc.Add(uint64(val.n[3]) * uint64(val2.n[3])) - acc.Add(uint64(val.n[4]) * uint64(val2.n[2])) - acc.Add(uint64(val.n[5]) * uint64(val2.n[1])) - acc.Add(uint64(val.n[6]) * uint64(val2.n[0])) - t6 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*7). - acc.Add(uint64(val.n[0]) * uint64(val2.n[7])) - acc.Add(uint64(val.n[1]) * uint64(val2.n[6])) - acc.Add(uint64(val.n[2]) * uint64(val2.n[5])) - acc.Add(uint64(val.n[3]) * uint64(val2.n[4])) - acc.Add(uint64(val.n[4]) * uint64(val2.n[3])) - acc.Add(uint64(val.n[5]) * uint64(val2.n[2])) - acc.Add(uint64(val.n[6]) * uint64(val2.n[1])) - acc.Add(uint64(val.n[7]) * uint64(val2.n[0])) - t7 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*8). - acc.Add(uint64(val.n[1]) * uint64(val2.n[7])) - acc.Add(uint64(val.n[2]) * uint64(val2.n[6])) - acc.Add(uint64(val.n[3]) * uint64(val2.n[5])) - acc.Add(uint64(val.n[4]) * uint64(val2.n[4])) - acc.Add(uint64(val.n[5]) * uint64(val2.n[3])) - acc.Add(uint64(val.n[6]) * uint64(val2.n[2])) - acc.Add(uint64(val.n[7]) * uint64(val2.n[1])) - t8 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*9). - acc.Add(uint64(val.n[2]) * uint64(val2.n[7])) - acc.Add(uint64(val.n[3]) * uint64(val2.n[6])) - acc.Add(uint64(val.n[4]) * uint64(val2.n[5])) - acc.Add(uint64(val.n[5]) * uint64(val2.n[4])) - acc.Add(uint64(val.n[6]) * uint64(val2.n[3])) - acc.Add(uint64(val.n[7]) * uint64(val2.n[2])) - t9 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*10). - acc.Add(uint64(val.n[3]) * uint64(val2.n[7])) - acc.Add(uint64(val.n[4]) * uint64(val2.n[6])) - acc.Add(uint64(val.n[5]) * uint64(val2.n[5])) - acc.Add(uint64(val.n[6]) * uint64(val2.n[4])) - acc.Add(uint64(val.n[7]) * uint64(val2.n[3])) - t10 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*11). - acc.Add(uint64(val.n[4]) * uint64(val2.n[7])) - acc.Add(uint64(val.n[5]) * uint64(val2.n[6])) - acc.Add(uint64(val.n[6]) * uint64(val2.n[5])) - acc.Add(uint64(val.n[7]) * uint64(val2.n[4])) - t11 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*12). - acc.Add(uint64(val.n[5]) * uint64(val2.n[7])) - acc.Add(uint64(val.n[6]) * uint64(val2.n[6])) - acc.Add(uint64(val.n[7]) * uint64(val2.n[5])) - t12 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*13). - acc.Add(uint64(val.n[6]) * uint64(val2.n[7])) - acc.Add(uint64(val.n[7]) * uint64(val2.n[6])) - t13 := uint64(acc.n[0]) - acc.Rsh32() - - // Terms for 2^(32*14). - acc.Add(uint64(val.n[7]) * uint64(val2.n[7])) - t14 := uint64(acc.n[0]) - acc.Rsh32() - - // What's left is for 2^(32*15). - t15 := uint64(acc.n[0]) - // acc.Rsh32() // No need since not used after this. Guaranteed to be 0. - - // At this point, all of the terms are grouped into their respective base - // and occupy up to 512 bits. Reduce the result accordingly. - s.reduce512(t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, - t15) +func (s *ModNScalar) Mul2(a, b *ModNScalar) *ModNScalar { + var product [8]uint64 + field64Mul512(&product, &a.n, &b.n) + scalar64Reduce512(&s.n, &product) return s } @@ -938,15 +760,10 @@ func (s *ModNScalar) Mul(val *ModNScalar) *ModNScalar { // The scalar is returned to support chaining. This enables syntax like: // s3.SquareVal(s).Mul(s) so that s3 = s^2 * s = s^3. func (s *ModNScalar) SquareVal(val *ModNScalar) *ModNScalar { - // This could technically be optimized slightly to take advantage of the - // fact that many of the intermediate calculations in squaring are just - // doubling, however, benchmarking has shown that due to the need to use a - // 96-bit accumulator, any savings are essentially offset by that and - // consequently there is no real difference in performance over just - // multiplying the value by itself to justify the extra code for now. This - // can be revisited in the future if it becomes a bottleneck in practice. - - return s.Mul2(val, val) + var product [8]uint64 + field64Square512(&product, &val.n) + scalar64Reduce512(&s.n, &product) + return s } // Square squares the scalar modulo the group order in constant time. The @@ -969,32 +786,38 @@ func (s *ModNScalar) NegateVal(val *ModNScalar) *ModNScalar { // minus the value. This implies that the result will always be in the // desired range with the sole exception of 0 because N - 0 = N itself. // - // Therefore, in order to avoid the need to reduce the result for every - // other case in order to achieve constant time, this creates a mask that is - // all 0s in the case of the scalar being negated is 0 and all 1s otherwise - // and bitwise ands that mask with each word. - // - // Finally, to simplify the carry propagation, this adds the two's - // complement of the scalar to N in order to achieve the same result. - bits := val.n[0] | val.n[1] | val.n[2] | val.n[3] | val.n[4] | val.n[5] | - val.n[6] | val.n[7] - mask := uint64(uint32Mask * constantTimeNotEq(bits, 0)) - c := uint64(orderWordZero) + (uint64(^val.n[0]) + 1) - s.n[0] = uint32(c & mask) - c = (c >> 32) + uint64(orderWordOne) + uint64(^val.n[1]) - s.n[1] = uint32(c & mask) - c = (c >> 32) + uint64(orderWordTwo) + uint64(^val.n[2]) - s.n[2] = uint32(c & mask) - c = (c >> 32) + uint64(orderWordThree) + uint64(^val.n[3]) - s.n[3] = uint32(c & mask) - c = (c >> 32) + uint64(orderWordFour) + uint64(^val.n[4]) - s.n[4] = uint32(c & mask) - c = (c >> 32) + uint64(orderWordFive) + uint64(^val.n[5]) - s.n[5] = uint32(c & mask) - c = (c >> 32) + uint64(orderWordSix) + uint64(^val.n[6]) - s.n[6] = uint32(c & mask) - c = (c >> 32) + uint64(orderWordSeven) + uint64(^val.n[7]) - s.n[7] = uint32(c & mask) + // The following handles that case in constant time by creating a mask that + // is all 0s in the case the scalar being negated is 0 and all 1s otherwise + // and then bitwise ands that mask with each limb. + // + // This approach was chosen over subtracting from 0 and then conditionally + // adding N because it's over twice as fast on 32-bit hardware while only + // being about 3-4% slower on 64-bit hardware. + // + // Determine mask first to allow aliasing. + mask := -uint64(constantTimeNotEq64(val.n[0]|val.n[1]|val.n[2]|val.n[3], 0)) + + // Unconditionally subtract the scalar from the group order. + // + // To simplify the carry propagation, this adds the two's complement of the + // scalar to N in order to achieve the same result. + // + // s = N - val + var c uint64 + s.n[0], c = bits.Add64(orderLimb0, ^val.n[0], 1) + s.n[1], c = bits.Add64(orderLimb1, ^val.n[1], c) + s.n[2], c = bits.Add64(orderLimb2, ^val.n[2], c) + s.n[3], _ = bits.Add64(orderLimb3, ^val.n[3], c) + + // Either keep the result when val != 0 or clear it when val == 0. The + // result is either: + // + // val == 0: s = 0 + // val != 0: s = N - val + s.n[0] &= mask + s.n[1] &= mask + s.n[2] &= mask + s.n[3] &= mask return s } @@ -1066,26 +889,42 @@ func (s *ModNScalar) InverseNonConst() *ModNScalar { // IsOverHalfOrder returns whether or not the scalar exceeds the group order // divided by 2 in constant time. func (s *ModNScalar) IsOverHalfOrder() bool { - // The intuition here is that the scalar is greater than half of the group - // order if one of the higher individual words is greater than the - // corresponding word of the half group order and all higher words in the - // scalar are equal to their corresponding word of the half group order. - // - // Note that the words 4, 5, and 6 are all the max uint32 value, so there is - // no need to test if those individual words of the scalar exceeds them, - // hence, only equality is checked for them. - result := constantTimeGreater(s.n[7], halfOrderWordSeven) - highWordsEqual := constantTimeEq(s.n[7], halfOrderWordSeven) - highWordsEqual &= constantTimeEq(s.n[6], halfOrderWordSix) - highWordsEqual &= constantTimeEq(s.n[5], halfOrderWordFive) - highWordsEqual &= constantTimeEq(s.n[4], halfOrderWordFour) - result |= highWordsEqual & constantTimeGreater(s.n[3], halfOrderWordThree) - highWordsEqual &= constantTimeEq(s.n[3], halfOrderWordThree) - result |= highWordsEqual & constantTimeGreater(s.n[2], halfOrderWordTwo) - highWordsEqual &= constantTimeEq(s.n[2], halfOrderWordTwo) - result |= highWordsEqual & constantTimeGreater(s.n[1], halfOrderWordOne) - highWordsEqual &= constantTimeEq(s.n[1], halfOrderWordOne) - result |= highWordsEqual & constantTimeGreater(s.n[0], halfOrderWordZero) - - return result != 0 + // These fields provide convenient access to each of the limbs of the + // secp256k1 curve group order N / 2 to improve code readability and avoid + // the need to recalculate them. + // + // The half order of the secp256k1 curve group is: + // 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0 + // + // Converting that to field representation (base 2^64) is: + // + // n[0] = 0xdfe92f46681b20a0 + // n[1] = 0x5d576e7357a4501d + // n[2] = 0xffffffffffffffff + // n[3] = 0x7fffffffffffffff + // + // This can be verified with the following test code: + // halfOrder := new(big.Int).Div(curveParams.N, big.NewInt(2)) + // var s ModNScalar + // s.SetByteSlice(halfOrder.Bytes()) + // t.Logf("%x", s.n) + const ( + halfOrderLimb0 uint64 = 0xdfe92f46681b20a0 + halfOrderLimb1 uint64 = 0x5d576e7357a4501d + halfOrderLimb2 uint64 = 0xffffffffffffffff + halfOrderLimb3 uint64 = 0x7fffffffffffffff + ) + + // The goal is to return true when the scalar is greater than half of the + // group order. That is, return true when s > N/2, which is trivially + // rearranged to s - (N/2 + 1) ≥ 0. + // + // In other words, the condition is met iff subtracting (N/2 + 1) from s is + // non-negative (aka there was no borrow). + var borrow uint64 + _, borrow = bits.Sub64(s.n[0], halfOrderLimb0+1, borrow) + _, borrow = bits.Sub64(s.n[1], halfOrderLimb1, borrow) + _, borrow = bits.Sub64(s.n[2], halfOrderLimb2, borrow) + _, borrow = bits.Sub64(s.n[3], halfOrderLimb3, borrow) + return borrow == 0 } diff --git a/dcrec/secp256k1/modnscalar_test.go b/dcrec/secp256k1/modnscalar_test.go index 448378e0cf..94e329f237 100644 --- a/dcrec/secp256k1/modnscalar_test.go +++ b/dcrec/secp256k1/modnscalar_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2023 The Decred developers +// Copyright (c) 2020-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -116,23 +116,27 @@ func TestModNScalarSetInt(t *testing.T) { tests := []struct { name string // test description in uint32 // test value - expected [8]uint32 // expected raw ints + expected [4]uint64 // expected limbs }{{ + name: "zero", + in: 0, + expected: [4]uint64{0, 0, 0, 0}, + }, { name: "five", in: 5, - expected: [8]uint32{5, 0, 0, 0, 0, 0, 0, 0}, + expected: [4]uint64{5, 0, 0, 0}, }, { - name: "group order word zero", - in: orderWordZero, - expected: [8]uint32{orderWordZero, 0, 0, 0, 0, 0, 0, 0}, + name: "max uint8 (2^8 - 1)", + in: 255, + expected: [4]uint64{255, 0, 0, 0}, }, { - name: "group order word zero + 1", - in: orderWordZero + 1, - expected: [8]uint32{orderWordZero + 1, 0, 0, 0, 0, 0, 0, 0}, + name: "max uint16 (2^16 - 1)", + in: 65535, + expected: [4]uint64{65535, 0, 0, 0}, }, { - name: "2^32 - 1", + name: "max uint32 (2^32 - 1)", in: 4294967295, - expected: [8]uint32{4294967295, 0, 0, 0, 0, 0, 0, 0}, + expected: [4]uint64{4294967295, 0, 0, 0}, }} for _, test := range tests { @@ -152,88 +156,136 @@ func TestModNScalarSetBytes(t *testing.T) { tests := []struct { name string // test description in string // hex encoded test value - expected [8]uint32 // expected raw ints + expected [4]uint64 // expected raw ints overflow bool // expected overflow result }{{ name: "zero", in: "00", - expected: [8]uint32{0, 0, 0, 0, 0, 0, 0, 0}, + expected: [4]uint64{0, 0, 0, 0}, + overflow: false, + }, { + name: "one (only limb zero set)", + in: "01", + expected: [4]uint64{1, 0, 0, 0}, + overflow: false, + }, { + name: "2^64 - 1 (limb zero all ones)", + in: "ffffffffffffffff", + expected: [4]uint64{0xffffffffffffffff, 0, 0, 0}, + overflow: false, + }, { + name: "2^64 (limb one low bit)", + in: "010000000000000000", + expected: [4]uint64{0, 1, 0, 0}, + overflow: false, + }, { + name: "2^128 - 1 (limbs zero and one all ones)", + in: "ffffffffffffffffffffffffffffffff", + expected: [4]uint64{0xffffffffffffffff, 0xffffffffffffffff, 0, 0}, + overflow: false, + }, { + name: "2^128 (limb two low bit)", + in: "0100000000000000000000000000000000", + expected: [4]uint64{0, 0, 1, 0}, + overflow: false, + }, { + name: "2^192 - 1 (limbs zero, one, and two all ones)", + in: "ffffffffffffffffffffffffffffffffffffffffffffffff", + expected: [4]uint64{ + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0, + }, + overflow: false, + }, { + name: "2^192 (limb three low bit)", + in: "01000000000000000000000000000000000000000000000000", + expected: [4]uint64{0, 0, 0, 1}, + overflow: false, + }, { + name: "2^255 (limb three high bit)", + in: "8000000000000000000000000000000000000000000000000000000000000000", + expected: [4]uint64{0, 0, 0, 0x8000000000000000}, + overflow: false, + }, { + name: "distinct value in every limb (verifies limb ordering)", + in: "0123456789abcdeffedcba98765432100f1e2d3c4b5a69788796a5b4c3d2e1f0", + expected: [4]uint64{ + 0x08796a5b4c3d2e1f0, 0x0f1e2d3c4b5a6978, 0xfedcba9876543210, + 0x0123456789abcdef, + }, overflow: false, }, { name: "group order (aka 0)", in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", - expected: [8]uint32{0, 0, 0, 0, 0, 0, 0, 0}, + expected: [4]uint64{0, 0, 0, 0}, overflow: true, }, { - name: "group order - 1", + name: "group order - 1 (limb zero one below prime, upper limbs maxed)", in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", - expected: [8]uint32{ - 0xd0364140, 0xbfd25e8c, 0xaf48a03b, 0xbaaedce6, - 0xfffffffe, 0xffffffff, 0xffffffff, 0xffffffff, + expected: [4]uint64{ + 0xbfd25e8cd0364140, 0xbaaedce6af48a03b, 0xfffffffffffffffe, + 0xffffffffffffffff, }, overflow: false, }, { - name: "group order + 1 (aka 1, overflow in word zero)", + name: "group order + 1 (aka 1, overflow in limb zero)", in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142", - expected: [8]uint32{1, 0, 0, 0, 0, 0, 0, 0}, + expected: [4]uint64{1, 0, 0, 0}, overflow: true, }, { - name: "group order word zero", - in: "d0364141", - expected: [8]uint32{0xd0364141, 0, 0, 0, 0, 0, 0, 0}, - overflow: false, - }, { - name: "group order word zero and one", + name: "group order limb zero", in: "bfd25e8cd0364141", - expected: [8]uint32{0xd0364141, 0xbfd25e8c, 0, 0, 0, 0, 0, 0}, + expected: [4]uint64{0xbfd25e8cd0364141, 0, 0, 0}, overflow: false, }, { - name: "group order words zero, one, and two", - in: "af48a03bbfd25e8cd0364141", - expected: [8]uint32{0xd0364141, 0xbfd25e8c, 0xaf48a03b, 0, 0, 0, 0, 0}, + name: "group order limbs zero and one", + in: "baaedce6af48a03bbfd25e8cd0364141", + expected: [4]uint64{0xbfd25e8cd0364141, 0xbaaedce6af48a03b, 0, 0}, overflow: false, }, { - name: "overflow in word one", - in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8dd0364141", - expected: [8]uint32{0, 1, 0, 0, 0, 0, 0, 0}, - overflow: true, + name: "group order limbs zero, one, and two", + in: "fffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + expected: [4]uint64{ + 0xbfd25e8cd0364141, 0xbaaedce6af48a03b, 0xfffffffffffffffe, 0, + }, + overflow: false, }, { - name: "overflow in word two", + name: "group order + 2^64 (overflow in limb one)", in: "fffffffffffffffffffffffffffffffebaaedce6af48a03cbfd25e8cd0364141", - expected: [8]uint32{0, 0, 1, 0, 0, 0, 0, 0}, + expected: [4]uint64{0, 1, 0, 0}, overflow: true, }, { - name: "overflow in word three", - in: "fffffffffffffffffffffffffffffffebaaedce7af48a03bbfd25e8cd0364141", - expected: [8]uint32{0, 0, 0, 1, 0, 0, 0, 0}, + name: "group order + 2^128 (overflow in limb two)", + in: "ffffffffffffffffffffffffffffffffbaaedce6af48a03bbfd25e8cd0364141", + expected: [4]uint64{0, 0, 1, 0}, overflow: true, }, { - name: "overflow in word four", - in: "ffffffffffffffffffffffffffffffffbaaedce6af48a03bbfd25e8cd0364141", - expected: [8]uint32{0, 0, 0, 0, 1, 0, 0, 0}, + name: "2^256 - 1 (max input)", + in: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + expected: [4]uint64{0x402da1732fc9bebe, 0x4551231950b75fc4, 1, 0}, + overflow: true, }, { name: "(group order - 1) * 2 NOT mod N, truncated >32 bytes", in: "01fffffffffffffffffffffffffffffffd755db9cd5e9140777fa4bd19a06c8284", - expected: [8]uint32{ - 0x19a06c82, 0x777fa4bd, 0xcd5e9140, 0xfd755db9, - 0xffffffff, 0xffffffff, 0xffffffff, 0x01ffffff, + expected: [4]uint64{ + 0x777fa4bd19a06c82, 0xfd755db9cd5e9140, 0xffffffffffffffff, + 0x01ffffffffffffff, }, overflow: false, }, { - name: "alternating bits", + name: "alternating bits (spans all limbs)", in: "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", - expected: [8]uint32{ - 0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5, - 0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5, + expected: [4]uint64{ + 0xa5a5a5a5a5a5a5a5, 0xa5a5a5a5a5a5a5a5, 0xa5a5a5a5a5a5a5a5, + 0xa5a5a5a5a5a5a5a5, }, overflow: false, }, { - name: "alternating bits 2", + name: "alternating bits 2 (spans all limbs)", in: "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", - expected: [8]uint32{ - 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, - 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, + expected: [4]uint64{ + 0x5a5a5a5a5a5a5a5a, 0x5a5a5a5a5a5a5a5a, 0x5a5a5a5a5a5a5a5a, + 0x5a5a5a5a5a5a5a5a, }, overflow: false, }} @@ -305,35 +357,35 @@ func TestModNScalarBytes(t *testing.T) { in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", expected: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", }, { - name: "group order + 1 (aka 1, overflow in word zero)", + name: "group order + 1 (aka 1, overflow in limb zero)", in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142", expected: "0000000000000000000000000000000000000000000000000000000000000001", }, { - name: "group order word zero", + name: "group order limb zero", in: "d0364141", expected: "00000000000000000000000000000000000000000000000000000000d0364141", }, { - name: "group order word zero and one", + name: "group order limb zero and one", in: "bfd25e8cd0364141", expected: "000000000000000000000000000000000000000000000000bfd25e8cd0364141", }, { - name: "group order words zero, one, and two", + name: "group order limbs zero, one, and two", in: "af48a03bbfd25e8cd0364141", expected: "0000000000000000000000000000000000000000af48a03bbfd25e8cd0364141", }, { - name: "overflow in word one", + name: "overflow in limb one", in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8dd0364141", expected: "0000000000000000000000000000000000000000000000000000000100000000", }, { - name: "overflow in word two", + name: "overflow in limb two", in: "fffffffffffffffffffffffffffffffebaaedce6af48a03cbfd25e8cd0364141", expected: "0000000000000000000000000000000000000000000000010000000000000000", }, { - name: "overflow in word three", + name: "overflow in limb three", in: "fffffffffffffffffffffffffffffffebaaedce7af48a03bbfd25e8cd0364141", expected: "0000000000000000000000000000000000000001000000000000000000000000", }, { - name: "overflow in word four", + name: "overflow in limb four", in: "ffffffffffffffffffffffffffffffffbaaedce6af48a03bbfd25e8cd0364141", expected: "0000000000000000000000000000000100000000000000000000000000000000", }, { @@ -501,11 +553,11 @@ func TestModNScalarEqualsRandom(t *testing.T) { t.Fatalf("failed equality check\nscalar in: %v", s) } - // Flip a random bit in a random word and ensure it's no longer equal. - randomWord := rng.Int31n(int32(len(s.n))) - randomBit := uint32(1 << uint32(rng.Int31n(32))) + // Flip a random bit in a random limb and ensure it's no longer equal. + randomLimb := rng.Int31n(int32(len(s.n))) + randomBit := uint64(1 << uint64(rng.Int63n(64))) s2 := new(ModNScalar).Set(s) - s2.n[randomWord] ^= randomBit + s2.n[randomLimb] ^= randomBit if s2.Equals(s) { t.Fatalf("failed inequality check\nscalar in: %v", s2) } @@ -531,55 +583,35 @@ func TestModNScalarAdd(t *testing.T) { in2: "0", expected: "1", }, { - name: "group order (aka 0) + 1 (gets reduced, no overflow)", - in1: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", - in2: "1", - expected: "1", - }, { - name: "group order - 1 + 1 (aka 0, overflow to prime)", + name: "group order-1 + 1 (aka 0, overflow to order)", in1: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", in2: "1", expected: "0", }, { - name: "group order - 1 + 2 (aka 1, overflow in word zero)", - in1: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", - in2: "2", + name: "group order (aka 0) + 1 (aka 1, gets reduced, no overflow)", + in1: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + in2: "1", expected: "1", }, { - name: "overflow in word one", - in1: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8bd0364141", - in2: "100000001", + name: "group order-1 + 2 (aka 1, overflow in limb zero)", + in1: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", + in2: "2", expected: "1", }, { - name: "overflow in word two", + name: "overflow in limb one", in1: "fffffffffffffffffffffffffffffffebaaedce6af48a03abfd25e8cd0364141", in2: "10000000000000001", expected: "1", }, { - name: "overflow in word three", - in1: "fffffffffffffffffffffffffffffffebaaedce5af48a03bbfd25e8cd0364141", - in2: "1000000000000000000000001", - expected: "1", - }, { - name: "overflow in word four", + name: "overflow in limb two", in1: "fffffffffffffffffffffffffffffffdbaaedce6af48a03bbfd25e8cd0364141", in2: "100000000000000000000000000000001", expected: "1", }, { - name: "overflow in word five", - in1: "fffffffffffffffffffffffefffffffebaaedce6af48a03bbfd25e8cd0364141", - in2: "10000000000000000000000000000000000000001", - expected: "1", - }, { - name: "overflow in word six", + name: "overflow in limb three", in1: "fffffffffffffffefffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", in2: "1000000000000000000000000000000000000000000000001", expected: "1", - }, { - name: "overflow in word seven", - in1: "fffffffefffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", - in2: "100000000000000000000000000000000000000000000000000000001", - expected: "1", }, { name: "alternating bits", in1: "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", @@ -601,7 +633,7 @@ func TestModNScalarAdd(t *testing.T) { // Ensure the result has the expected value. s1.Add(s2) if !s1.Equals(expected) { - t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, + t.Errorf("%s: unexpected result\ngot: %v\nwant: %v", test.name, s1, expected) continue } @@ -645,53 +677,6 @@ func TestModNScalarAddRandom(t *testing.T) { } } -// TestAccumulator96Add ensures that the internal 96-bit accumulator used by -// multiplication works as expected for overflow edge cases including overflow. -func TestAccumulator96Add(t *testing.T) { - tests := []struct { - name string // test description - start accumulator96 // starting value of accumulator - in uint64 // value to add to accumulator - expected accumulator96 // expected value of accumulator after addition - }{{ - name: "0 + 0 = 0", - start: accumulator96{[3]uint32{0, 0, 0}}, - in: 0, - expected: accumulator96{[3]uint32{0, 0, 0}}, - }, { - name: "overflow in word zero", - start: accumulator96{[3]uint32{0xffffffff, 0, 0}}, - in: 1, - expected: accumulator96{[3]uint32{0, 1, 0}}, - }, { - name: "overflow in word one", - start: accumulator96{[3]uint32{0, 0xffffffff, 0}}, - in: 0x100000000, - expected: accumulator96{[3]uint32{0, 0, 1}}, - }, { - name: "overflow in words one and two", - start: accumulator96{[3]uint32{0xffffffff, 0xffffffff, 0}}, - in: 1, - expected: accumulator96{[3]uint32{0, 0, 1}}, - }, { - // Start accumulator at 129127208455837319175 which is the result of - // 4294967295 * 4294967295 accumulated seven times. - name: "max result from eight adds of max uint32 multiplications", - start: accumulator96{[3]uint32{7, 4294967282, 6}}, - in: 18446744065119617025, - expected: accumulator96{[3]uint32{8, 4294967280, 7}}, - }} - - for _, test := range tests { - acc := test.start - acc.Add(test.in) - if acc.n != test.expected.n { - t.Errorf("%s: wrong result\ngot: %v\nwant: %v", test.name, acc.n, - test.expected.n) - } - } -} - // TestModNScalarMul ensures that multiplying two scalars together works as // expected for edge cases. func TestModNScalarMul(t *testing.T) { @@ -720,6 +705,16 @@ func TestModNScalarMul(t *testing.T) { in1: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", in2: "2", expected: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413f", + }, { + name: "(group order-1) * 3", + in1: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", + in2: "3", + expected: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e", + }, { + name: "(group order-1) * 4", + in1: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", + in2: "4", + expected: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413d", }, { name: "(group order-1) * (group order-1)", in1: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", @@ -730,56 +725,76 @@ func TestModNScalarMul(t *testing.T) { in1: "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a1", in2: "2", expected: "1", + }, { + name: "highest bit in limb zero squared", + in1: "8000000000000000", + in2: "8000000000000000", + expected: "40000000000000000000000000000000", + }, { + name: "highest bit in limb one squared", + in1: "80000000000000000000000000000000", + in2: "80000000000000000000000000000000", + expected: "4000000000000000000000000000000000000000000000000000000000000000", + }, { + name: "highest bit in limb two squared", + in1: "800000000000000000000000000000000000000000000000", + in2: "800000000000000000000000000000000000000000000000", + expected: "515448c6542dd7f1100b685ccbf26fafc0000000000000000000000000000000", + }, { + name: "highest bit in limb three squared", + in1: "8000000000000000000000000000000000000000000000000000000000000000", + in2: "8000000000000000000000000000000000000000000000000000000000000000", + expected: "2759c7356071a6f179a5fd7916f341f19d0525b0839f3e1e225b3c8519f5f450", + }, { + name: "cross limb product 64x128", + in1: "10000000000000000", + in2: "100000000000000000000000000000000", + expected: "1000000000000000000000000000000000000000000000000", + }, { + name: "cross limb product 64x192", + in1: "10000000000000000", + in2: "1000000000000000000000000000000000000000000000000", + expected: "14551231950b75fc4402da1732fc9bebf", + }, { + name: "cross limb product 128x192", + in1: "100000000000000000000000000000000", + in2: "1000000000000000000000000000000000000000000000000", + expected: "14551231950b75fc4402da1732fc9bebf0000000000000000", }, { name: "group order (aka 0) * 3", in1: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", in2: "3", expected: "0", }, { - name: "overflow in word eight", + name: "overflow in limb four", in1: "100000000000000000000000000000000", in2: "100000000000000000000000000000000", expected: "14551231950b75fc4402da1732fc9bebf", }, { - name: "overflow in word nine", - in1: "1000000000000000000000000000000000000", - in2: "1000000000000000000000000000000000000", - expected: "14551231950b75fc4402da1732fc9bebf00000000", - }, { - name: "overflow in word ten", + name: "overflow in limb five", in1: "10000000000000000000000000000000000000000", in2: "10000000000000000000000000000000000000000", expected: "14551231950b75fc4402da1732fc9bebf0000000000000000", }, { - name: "overflow in word eleven", - in1: "100000000000000000000000000000000000000000000", - in2: "100000000000000000000000000000000000000000000", - expected: "14551231950b75fc4402da1732fc9bebf000000000000000000000000", - }, { - name: "overflow in word twelve", + name: "overflow in limb six", in1: "1000000000000000000000000000000000000000000000000", in2: "1000000000000000000000000000000000000000000000000", expected: "4551231950b75fc4402da1732fc9bec04551231950b75fc4402da1732fc9bebf", }, { - name: "overflow in word thirteen", - in1: "10000000000000000000000000000000000000000000000000000", - in2: "10000000000000000000000000000000000000000000000000000", - expected: "50b75fc4402da1732fc9bec09d671cd51b343a1b66926b57d2a4c1c61536bda7", - }, { - name: "overflow in word fourteen", + name: "overflow in limb seven", in1: "100000000000000000000000000000000000000000000000000000000", in2: "100000000000000000000000000000000000000000000000000000000", expected: "402da1732fc9bec09d671cd581c69bc59509b0b074ec0aea8f564d667ec7eb3c", }, { - name: "overflow in word fifteen", - in1: "1000000000000000000000000000000000000000000000000000000000000", - in2: "1000000000000000000000000000000000000000000000000000000000000", - expected: "2fc9bec09d671cd581c69bc5e697f5e41f12c33a0a7b6f4e3302b92ea029cecd", + name: "max limb * one", + in1: "ffffffffffffffff", + in2: "1", + expected: "ffffffffffffffff", }, { - name: "double overflow in internal accumulator", - in1: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", - in2: "55555555555555555555555555555554e8e4f44ce51835693ff0ca2ef01215c2", - expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b7f", + name: "max limb * max limb", + in1: "ffffffffffffffff", + in2: "ffffffffffffffff", + expected: "fffffffffffffffe0000000000000001", }, { name: "alternating bits", in1: "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", @@ -881,35 +896,35 @@ func TestModNScalarSquare(t *testing.T) { in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142", expected: "1", }, { - name: "overflow in word eight", + name: "overflow in limb eight", in: "100000000000000000000000000000000", expected: "14551231950b75fc4402da1732fc9bebf", }, { - name: "overflow in word nine", + name: "overflow in limb nine", in: "1000000000000000000000000000000000000", expected: "14551231950b75fc4402da1732fc9bebf00000000", }, { - name: "overflow in word ten", + name: "overflow in limb ten", in: "10000000000000000000000000000000000000000", expected: "14551231950b75fc4402da1732fc9bebf0000000000000000", }, { - name: "overflow in word eleven", + name: "overflow in limb eleven", in: "100000000000000000000000000000000000000000000", expected: "14551231950b75fc4402da1732fc9bebf000000000000000000000000", }, { - name: "overflow in word twelve", + name: "overflow in limb twelve", in: "1000000000000000000000000000000000000000000000000", expected: "4551231950b75fc4402da1732fc9bec04551231950b75fc4402da1732fc9bebf", }, { - name: "overflow in word thirteen", + name: "overflow in limb thirteen", in: "10000000000000000000000000000000000000000000000000000", expected: "50b75fc4402da1732fc9bec09d671cd51b343a1b66926b57d2a4c1c61536bda7", }, { - name: "overflow in word fourteen", + name: "overflow in limb fourteen", in: "100000000000000000000000000000000000000000000000000000000", expected: "402da1732fc9bec09d671cd581c69bc59509b0b074ec0aea8f564d667ec7eb3c", }, { - name: "overflow in word fifteen", + name: "overflow in limb fifteen", in: "1000000000000000000000000000000000000000000000000000000000000", expected: "2fc9bec09d671cd581c69bc5e697f5e41f12c33a0a7b6f4e3302b92ea029cecd", }, { @@ -1015,33 +1030,41 @@ func TestModNScalarNegate(t *testing.T) { in: "1", expected: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", }, { - name: "negation in word one", - in: "0000000000000000000000000000000000000000000000000000000100000000", - expected: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8bd0364141", + name: "two", + in: "2", + expected: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413f", + }, { + name: "three", + in: "3", + expected: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e", }, { - name: "negation in word two", + name: "group order - 2", + in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413f", + expected: "2", + }, { + name: "group order - 1", + in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", + expected: "1", + }, { + name: "negation in limb one", in: "0000000000000000000000000000000000000000000000010000000000000000", expected: "fffffffffffffffffffffffffffffffebaaedce6af48a03abfd25e8cd0364141", }, { - name: "negation in word three", - in: "0000000000000000000000000000000000000001000000000000000000000000", - expected: "fffffffffffffffffffffffffffffffebaaedce5af48a03bbfd25e8cd0364141", - }, { - name: "negation in word four", + name: "negation in limb two", in: "0000000000000000000000000000000100000000000000000000000000000000", expected: "fffffffffffffffffffffffffffffffdbaaedce6af48a03bbfd25e8cd0364141", }, { - name: "negation in word five", - in: "0000000000000000000000010000000000000000000000000000000000000000", - expected: "fffffffffffffffffffffffefffffffebaaedce6af48a03bbfd25e8cd0364141", - }, { - name: "negation in word six", + name: "negation in limb three", in: "0000000000000001000000000000000000000000000000000000000000000000", expected: "fffffffffffffffefffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", }, { - name: "negation in word seven", - in: "0000000100000000000000000000000000000000000000000000000000000000", - expected: "fffffffefffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + name: "half group order (floor)", + in: "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0", + expected: "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a1", + }, { + name: "half group order (ceil)", + in: "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a1", + expected: "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0", }, { name: "alternating bits", in: "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", @@ -1157,33 +1180,29 @@ func TestModNScalarInverseNonConst(t *testing.T) { in: "1", expected: "1", }, { - name: "inverse carry in word one", - in: "0000000000000000000000000000000000000000000000000000000100000000", - expected: "5588b13effffffffffffffffffffffff934e5b00ca8417bf50177f7ba415411a", + name: "two", + in: "2", + expected: "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a1", + }, { + name: "group order - 1", + in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", + expected: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", }, { - name: "inverse carry in word two", + name: "group order - 2", + in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413f", + expected: "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0", + }, { + name: "inverse carry in limb one", in: "0000000000000000000000000000000000000000000000010000000000000000", expected: "4b0dff665588b13effffffffffffffffa09f710af01555259d4ad302583de6dc", }, { - name: "inverse carry in word three", - in: "0000000000000000000000000000000000000001000000000000000000000000", - expected: "34b9ec244b0dff665588b13effffffffbcff4127932a971a78274c9d74176b38", - }, { - name: "inverse carry in word four", + name: "inverse carry in limb two", in: "0000000000000000000000000000000100000000000000000000000000000000", expected: "50a51ac834b9ec244b0dff665588b13e9984d5b3cf80ef0fd6a23766a3ee9f22", }, { - name: "inverse carry in word five", - in: "0000000000000000000000010000000000000000000000000000000000000000", - expected: "27cfab5e50a51ac834b9ec244b0dff6622f16e85b683d5a059bcd5a3b29d9dff", - }, { - name: "inverse carry in word six", + name: "inverse carry in limb three", in: "0000000000000001000000000000000000000000000000000000000000000000", expected: "897f30c127cfab5e50a51ac834b9ec239c53f268b4700c14f19b9499ac58d8ad", - }, { - name: "inverse carry in word seven", - in: "0000000100000000000000000000000000000000000000000000000000000000", - expected: "6494ef93897f30c127cfab5e50a51ac7b4e8f713e0cddd182234e907286ae6b3", }, { name: "alternating bits", in: "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", @@ -1308,21 +1327,33 @@ func TestModNScalarIsOverHalfOrder(t *testing.T) { in: "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a1", expected: true, }, { - name: "over half order word one", - in: "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f47681b20a0", + name: "over half order limb one", + in: "7fffffffffffffffffffffffffffffff5d576e7357a4501edfe92f46681b20a0", expected: true, }, { - name: "over half order word two", - in: "7fffffffffffffffffffffffffffffff5d576e7357a4501edfe92f46681b20a0", + name: "group order - 2", + in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413f", + expected: true, + }, { + name: "group order - 1", + in: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", expected: true, }, { - name: "over half order word three", - in: "7fffffffffffffffffffffffffffffff5d576e7457a4501ddfe92f46681b20a0", + name: "highest limb below half order", + in: "7fffffffffffffffffffffffffffffff00000000000000000000000000000000", + expected: false, + }, { + name: "highest bit set", + in: "8000000000000000000000000000000000000000000000000000000000000000", expected: true, }, { - name: "over half order word seven", - in: "8fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0", + name: "alternating bits", + in: "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", expected: true, + }, { + name: "alternating bits 2", + in: "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", + expected: false, }} for _, test := range tests { From 3d41b726e4df93740e7613a1477d510f4f9928ea Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 9 Jul 2026 01:28:34 -0500 Subject: [PATCH 06/33] secp256k1: Add scalar reduction proofs. This adds a formal verification proof of the scalar reduction arithmetic in the new 4x64 scalar implementation and updates the proofs README.md accordingly. It includes a formal proof for the following: - 512-bit modular reduction over the group order with saturated 64-bit limbs --- dcrec/secp256k1/internal/proofs/README.md | 9 +- .../proofs/modnscalar_4x64_reduce_prover.py | 571 ++++++++++++++++++ dcrec/secp256k1/modnscalar.go | 4 + 3 files changed, 582 insertions(+), 2 deletions(-) create mode 100644 dcrec/secp256k1/internal/proofs/modnscalar_4x64_reduce_prover.py diff --git a/dcrec/secp256k1/internal/proofs/README.md b/dcrec/secp256k1/internal/proofs/README.md index ba7e919f6e..079069a8f9 100644 --- a/dcrec/secp256k1/internal/proofs/README.md +++ b/dcrec/secp256k1/internal/proofs/README.md @@ -6,8 +6,9 @@ proofs This consists of formal verification artifacts used to help establish the correctness of the security-critical arithmetic implementations. -The current proofs formally verify properties of the optimized field arithmetic -implementations using [z3](https://github.com/Z3Prover/z3): +The current proofs formally verify properties of the optimized scalar and field +arithmetic implementations using the [Z3 Theorem +Prover](https://github.com/Z3Prover/z3): - Correctness of multi-precision multiplication - Bounds on intermediate values @@ -45,3 +46,7 @@ optimized implementations preserve the required arithmetic invariants. - [Field Modular Reduction](field_4x64_reduce_prover.py) Provides formal verification of the reduction of a 512-bit value represented using saturated 64-bit limbs modulo the secp256k1 prime. + +- [Scalar Modular Reduction](modnscalar_4x64_reduce_prover.py) + Provides formal verification of the reduction of a 512-bit value represented + using saturated 64-bit limbs modulo the secp256k1 group order. diff --git a/dcrec/secp256k1/internal/proofs/modnscalar_4x64_reduce_prover.py b/dcrec/secp256k1/internal/proofs/modnscalar_4x64_reduce_prover.py new file mode 100644 index 0000000000..1414738768 --- /dev/null +++ b/dcrec/secp256k1/internal/proofs/modnscalar_4x64_reduce_prover.py @@ -0,0 +1,571 @@ +# Copyright (c) 2026 The Decred developers +# Copyright (c) 2026 Dave Collins +# Use of this source code is governed by an ISC +# license that can be found in the LICENSE file. + +from z3_proof_helpers import * + +# ------- +# Inputs. +# ------- + +x = BitVecs('x0 x1 x2 x3 x4 x5 x6 x7', 64) + +# NOTE: The code in this section reuses the variable names from the Go code +# being proven for easy cross referencing. However, the comments refer to them +# by shorter mathematical variables for clarity. + +# Define N. +orderLimb0 = BitVecVal(0xbfd25e8cd0364141, 64) +orderLimb1 = BitVecVal(0xbaaedce6af48a03b, 64) +orderLimb2 = BitVecVal(0xfffffffffffffffe, 64) +orderLimb3 = BitVecVal(0xffffffffffffffff, 64) +order = Concat(orderLimb3, orderLimb2, orderLimb1, orderLimb0) + +# Define 2N. +twiceOrderLimb0 = BitVecVal(0x7fa4bd19a06c8282, 64) +twiceOrderLimb1 = BitVecVal(0x755db9cd5e914077, 64) +twiceOrderLimb2 = BitVecVal(0xfffffffffffffffd, 64) +twiceOrderLimb3 = BitVecVal(0xffffffffffffffff, 64) +twiceOrderLimb4 = ONE +twiceOrder = Concat( + twiceOrderLimb4, twiceOrderLimb3, twiceOrderLimb2, + twiceOrderLimb1, twiceOrderLimb0 +) + +# Define 2**256 - N. +orderComplementLimb0 = BitVecVal(0x402da1732fc9bebf, 64) +orderComplementLimb1 = BitVecVal(0x4551231950b75fc4, 64) +orderComplementLimb2 = ONE +orderComplement = Concat(orderComplementLimb2, orderComplementLimb1, orderComplementLimb0) + +# -------------------------------------------------------------------------- +# Prove the constant definitions satisfy the required identities before +# trusting anything that uses them. +# -------------------------------------------------------------------------- + +# Prove twiceOrder = 2N. +order320 = ZeroExt(64, order) +prove(twiceOrder == order320 << 1, "twiceOrder != 2N") + +# -------------------------------------------------------------------------- +# Lemma R-identity: N + c == 2**256 +# +# This lemma justifies replacing multiples of 2**256 with multiples of c when +# working modulo N: +# +# N + c ≡ 2**256 (mod N) +# => 0 + c ≡ 2**256 (mod N) +# => c ≡ 2**256 (mod N) +# -------------------------------------------------------------------------- +orderComplement320 = ZeroExt(128, orderComplement) +prove(order320+orderComplement320 == BitVecVal(1, 320)<<256, + "Lemma R-identity: N+c != 2**256") + +# --------------- +# Model the code. +# --------------- + +discards = [] + +# first reduction: 512 bits -> 385 bits. +h0, t0 = mul64(x[4], orderComplementLimb0) +h1, t1 = mul64(x[4], orderComplementLimb1) +t1, carry = add64(t1, h0, ZERO) +t2, carry = add64(x[4], h1, carry) +t3 = carry + +h0, l0 = mul64(x[5], orderComplementLimb0) +h1, l1 = mul64(x[5], orderComplementLimb1) +t1, carry = add64(t1, l0, ZERO) +t2, carry = add64(t2, h0, carry) +t3, discarded = add64(t3, h1, carry) +discards.append(discarded) +t2, carry = add64(t2, l1, ZERO) +t3, carry = add64(t3, x[5], carry) +t4 = carry + +h0, l0 = mul64(x[6], orderComplementLimb0) +h1, l1 = mul64(x[6], orderComplementLimb1) +t2, carry = add64(t2, l0, ZERO) +t3, carry = add64(t3, h0, carry) +t4, discarded = add64(t4, h1, carry) +discards.append(discarded) +t3, carry = add64(t3, l1, ZERO) +t4, carry = add64(t4, x[6], carry) +t5 = carry + +h0, l0 = mul64(x[7], orderComplementLimb0) +h1, l1 = mul64(x[7], orderComplementLimb1) +t3, carry = add64(t3, l0, ZERO) +t4, carry = add64(t4, h0, carry) +t5, discarded = add64(t5, h1, carry) +discards.append(discarded) +t4, carry = add64(t4, l1, ZERO) +t5, carry = add64(t5, x[7], carry) +t6 = carry + +t0, carry = add64(t0, x[0], ZERO) +t1, carry = add64(t1, x[1], carry) +t2, carry = add64(t2, x[2], carry) +t3, carry = add64(t3, x[3], carry) +t4, carry = add64(t4, ZERO, carry) +t5, carry = add64(t5, ZERO, carry) +t6, discarded = add64(t6, ZERO, carry) +discards.append(discarded) + +# Save for analysis. +t6_saved_1 = t6 +t_full_redux_1 = Concat(ZERO, t6, t5, t4, t3, t2, t1, t0) +first_redux_input_high = Concat(x[7], x[6], x[5], x[4]) + +# second reduction: 385 bits -> 258 bits. +h0, l0 = mul64(t4, orderComplementLimb0) +h1, l1 = mul64(t4, orderComplementLimb1) +l2 = t4 +t0, carry = add64(t0, l0, ZERO) +t1, carry = add64(t1, h0, carry) +t2, carry = add64(t2, h1, carry) +t3, carry = add64(t3, ZERO, carry) +t4 = carry +t1, carry = add64(t1, l1, ZERO) +t2, carry = add64(t2, l2, carry) +t3, carry = add64(t3, ZERO, carry) +t4, discarded = add64(t4, ZERO, carry) +discards.append(discarded) + +h0, l0 = mul64(t5, orderComplementLimb0) +h1, l1 = mul64(t5, orderComplementLimb1) +t1, carry = add64(t1, l0, ZERO) +t2, carry = add64(t2, h0, carry) +t3, carry = add64(t3, h1, carry) +t4, discarded = add64(t4, ZERO, carry) +discards.append(discarded) +t2, carry = add64(t2, l1, ZERO) +t3, carry = add64(t3, t5, carry) +t4, discarded = add64(t4, ZERO, carry) +discards.append(discarded) + +t2, carry = add64(t2, t6*orderComplementLimb0, ZERO) +t3, carry = add64(t3, t6*orderComplementLimb1, carry) +t4, discarded = add64(t4, t6, carry) +discards.append(discarded) + +# Save intermediate results for analysis. +t3_carry_2 = carry +t3_saved_2 = t3 +t4_saved_2 = t4 + +# third reduction: max 258 bits -> t < 2N. +t0, carry = add64(t0, t4*orderComplementLimb0, ZERO) +t1, carry = add64(t1, t4*orderComplementLimb1, carry) +t2, carry = add64(t2, t4, carry) +t3, carry = add64(t3, ZERO, carry) +t4 = carry + +# final reduction: t < 2N -> t < N +s0, borrow = sub64(t0, orderLimb0, ZERO) +s1, borrow = sub64(t1, orderLimb1, borrow) +s2, borrow = sub64(t2, orderLimb2, borrow) +s3, borrow = sub64(t3, orderLimb3, borrow) +_, borrow = sub64(t4, ZERO, borrow) +r0 = If(borrow == ONE, t0, s0) +r1 = If(borrow == ONE, t1, s1) +r2 = If(borrow == ONE, t2, s2) +r3 = If(borrow == ONE, t3, s3) + +# ------- +# Proofs. +# ------- + +# Discarded carries are never set. +prove_no_discarded_carries(discards) + +# Top limb after first reduction is at most 1 (t6 ≤ 1). +# +# This along with the proof there is no discarded carry on t6 after the first +# reduction proves the value does not exceed 385 bits (as expected since the +# complement is 129 bits and there were a max of 256 bits in the upper limbs +# before the first reduction, so 256 + 129 = 385). +prove(ULE(t6_saved_1, ONE), "top limb after 1st reduction > 1") + +# Top limb after second reduction is at most 3 (t4 ≤ 3). +# +# This along with the proof there is no discarded carry on t4 after the second +# reduction proves the value does not exceed 258 bits (as expected because the +# complement is 129 bits and there were 129 max bits left in the upper limbs +# after the first reduction, so 129 + 129 = 258). +prove(ULE(t4_saved_2, BitVecVal(3, 64)), "top limb after 2nd reduction > 3") + +# Top limb after second reduction times complement limb 0 never exceeds uint64. +# ((t4*c0)>>64 == 0) +# +# This proves the regular 64-bit product used in the third reduction does not +# overflow. +t4c0hi, _ = mul64(t4_saved_2, orderComplementLimb0) +prove( + t4c0hi == ZERO, + "high word of top limb from 2nd reduction times complement limb 0 != 0", +) + +# Top limb after second reduction times complement limb 1 never exceeds uint64. +# ((t4*c1)>>64 == 0) +# +# This proves the regular 64-bit product used in the third reduction does not +# overflow. +t4c1hi, _ = mul64(t4_saved_2, orderComplementLimb1) +prove( + t4c1hi == ZERO, + "high word of top limb from 2nd reduction times complement limb 1 != 0", +) + +# Top limb after third reduction is at most 1 (t4 ≤ 1). +# +# This proof is not strictly necessary since the bound is actually tigher per +# the next proof which implicitly proves this fact. However, it is fast to +# prove and including it provides nice symmetry with the previous proofs for the +# bounds of the top limb after each reduction. +prove(ULE(t4, ONE), "top limb after 3rd reduction > 1") + +# Value after third reduction is less than twice the group order (t < 2N). +t = Concat(t4, t3, t2, t1, t0) +prove(ULT(t, twiceOrder), "value after 3rd reduction >= 2N") + +# Fully reduced result is less than the group order (r < N). +r = Concat(r3, r2, r1, r0) +prove(ULT(r, order), "fully reduced result >= N") + +# ----------------------------------------------------------------------------- +# Prove functional congruence: r ≡ x (mod N). +# +# The checks above establish bounds and the safety of discarded carries, +# self-consistency of the constants, and the reduction identity (lemma +# R-identity). +# +# The following lemmas establish the arithmetic identity of each transformation +# stage. Together with the previously proven bounds and reduction identity, +# they imply the overall congruence r ≡ x (mod N). +# +# A direct assertion such as t == x_lo + (x >> 256)*P is intractable because +# it introduces a monolithic 256x256 multiplier that appears nowhere in the +# modeled code, so the SAT solver is forced into bit-level equivalence +# checking between two structurally unrelated multiplier circuits. +# +# Instead, because every accumulation block in the implementation is a fixed +# carry-propagation network over the *outputs* of bits.Mul64, whether or not +# those 128-bit inputs are actually products is irrelevant to the network. Each +# block satisfies its per-block identity for ALL 128-bit values. So each block +# identity is proven here as a universal lemma over unconstrained variables, +# which contains no multipliers at all and reduces to adder-network equivalence +# that the solver dispatches quickly. +# +# The Go function, scalar64Reduce512, computes r in four stages: +# +# 1) First reduction: Fold the high 256 bits of x (x4..x7) into the low 256 bits +# (x0..x3). This produces t_1 = x_lo + x_hi*c which fits in 385 bits (7 +# 64-bit limbs with the 7th limb small). +# 2) Second reduction: Fold t_1's 5th, 6th, and 7th limbs back in the same way. +# This produces t_2 = t_1_lo + t_1_hi*c which fits in 258 bits (5 64-bit +# limbs with the 7th limb small) +# 3) Third reduction: Fold t_2's 5th limb back in the same way. This produces +# t_3 = t_2_lo + t_1_hi*c, where t_3 < 2N. +# 4) Final reduction: One conditional subtraction of N which is valid because +# t_3 < 2N. +# +# The lemmas below chain together to prove r ≡ t3 ≡ t2 ≡ t1 ≡ x (mod N) and 0 ≤ +# r < N which is exactly the definition of x ≡ r (mod N). +def prove_functional_congruence_lemmas(): + """ + Prove r == x (mod N) for scalar64Reduce512. + + x is the 512-bit input (8 limbs x0..x7) and r is the fully-reduced 256-bit + output (4 limbs). + """ + + # Use a wide enough congruence width that nothing in a 512-bit input ever + # wraps around. This is comfortably wider than the largest quantity + # of x_hi*c < 2**385. + CONGRUENCE_WIDTH = 512 + + def cwidth(v): + """Zero-extend v to CONGRUENCE_WIDTH bits.""" + return ZeroExt(CONGRUENCE_WIDTH - v.size(), v) + + def pack(*limbs): + """ + Concatenate the provided big-endian 64-bit limbs into a single + CONGRUENCE_WIDTH-bit value: + + limb0*2**(64*num_limbs) + limb1*2**(64*(num_limbs-1)) + ... + """ + return cwidth(Concat(*limbs)) + + def pack_limbs(limbs): + """ + Concatenate the provided iterable little-endian 64-bit limbs into a + single CONGRUENCE_WIDTH-bit value: + + limbs[0] + limbs[1]*2**64 + limbs[2]*2**128 + ... + """ + return cwidth(Concat(*reversed(limbs))) + + def compose_mul_by_c(p0, p1, xj): + """ + Compose the multi-limb product xj*c from the two 128-bit partial + products produced by multiplying xj with the two complement limbs. + + Since the third complement limb is 1, the full product is: + + p0 + (p1 << 64) + (xj << 128) + + The result is equal to xj*c and is exactly how the implementation + accumulates it. + """ + return cwidth(p0) + (cwidth(p1)<<64) + (cwidth(xj)<<128) + + # Universal lemma variables shared by R-open, R-interior, and + # R-second-interior. + # + # x_j models an arbitrary 64-bit input limb. + # + # p0 and p1 model the 128-bit products returned by bits.Mul64(x[j], c0) and + # bits.Mul64(x[j], c1) (the lower two limbs of c) at some point in the real + # code. They are NOT tied to any specific x[j] or t[k]. + # + # h0 and l0 are the upper and lower halves of p0, respectively. Likewise h1 + # and l1 for p1. + # + # x_j_times_c is the composed value of x[j]*c. + x_j = BitVec("x_j", 64) + p0, p1 = BitVecs("p0 p1", 128) + h0, l0 = split128(p0) + h1, l1 = split128(p1) + x_j_times_c = compose_mul_by_c(p0, p1, x_j) + + # ------------------------------------------------------------------------- + # Lemma R-open: The opening row of the first reduction (the x4 line). + # + # h0, t0 = bits.Mul64(x[4], orderComplementLimb0) + # h1, t1 = bits.Mul64(x[4], orderComplementLimb1) + # t1, carry = bits.Add64(t1, h0, 0) + # t2, carry = bits.Add64(x[4], h1, carry) + # t3 = carry + # + # This only writes fresh limbs and has no discarded carries, so no + # assumptions are needed. + # ------------------------------------------------------------------------- + t0 = l0 + t1, carry = add64(l1, h0, ZERO) + t2, carry = add64(x_j, h1, carry) + t3 = carry + prove(pack(t3,t2,t1,t0) == x_j_times_c, "R-open: state != x4*c") + + # ------------------------------------------------------------------------- + # Lemma R-interior: The shape shared by the x5, x6, and x7 blocks in the + # first reduction. + # + # h0, l0 = bits.Mul64(x[j], orderComplementLimb0) + # h1, l1 = bits.Mul64(x[j], orderComplementLimb1) + # t[j], carry = bits.Add64(t[j], l0, 0) + # t[j+1], carry = bits.Add64(t[j+1], h0, carry) + # t[j+2], _ = bits.Add64(t[j+2], h1, carry) + # t[j+1], carry = bits.Add64(t[j+1], l1, 0) + # t[j+2], carry = bits.Add64(t[j+2], x[j], carry) + # t[j+3] = carry + # + # a0..a3 model the incoming running total in t0..t3. + # + # One carry is discarded and assumed zero. This fact is proven above by the + # discard checks for the real x5/x6/x7 blocks, so this does not smuggle in a + # new assumption. + # ------------------------------------------------------------------------- + a = BitVecs("a0 a1 a2", 64) + w = [None]*4 + w[0], carry = add64(a[0], l0, ZERO) + w[1], carry = add64(a[1], h0, carry) + w[2], discarded = add64(a[2], h1, carry) + w[1], carry = add64(w[1], l1, ZERO) + w[2], carry = add64(w[2], x_j, carry) + w[3] = carry + prove( + pack_limbs(w) == pack_limbs(a) + x_j_times_c, + "R-interior: block != state + xj*c", + assumptions=[discarded == ZERO], + ) + + # ------------------------------------------------------------------------- + # Lemma R-tail: Fold x0..x3 into running total after the blocks above. + # + # t0, carry = bits.Add64(t0, x[0], 0) + # t1, carry = bits.Add64(t1, x[1], carry) + # ... + # t5, carry = bits.Add64(t5, 0, carry) + # t6, _ = bits.Add64(t6, 0, carry) + # + # b0..b6 model the incoming running total in t0..t6. + # + # The final line is modeled here as add64 with its own carry-out assumed + # zero. This fact is proven above by the discard checks for the real total, + # so this does not smuggle in a new assumption. + # ------------------------------------------------------------------------- + b = BitVecs("b0 b1 b2 b3 b4 b5 b6", 64) + w = [None]*7 + w[0], carry = add64(b[0], x[0], ZERO) + w[1], carry = add64(b[1], x[1], carry) + w[2], carry = add64(b[2], x[2], carry) + w[3], carry = add64(b[3], x[3], carry) + w[4], carry = add64(b[4], ZERO, carry) + w[5], carry = add64(b[5], ZERO, carry) + w[6], discarded = add64(b[6], ZERO, carry) + prove( + pack_limbs(w) == pack_limbs(b) + pack_limbs(x[:4]), + "R-tail: post-fold total != state + x_lo", + assumptions=[discarded == ZERO], + ) + + # ------------------------------------------------------------------------- + # Lemma R-second-interior: The carry-propagation shape shared by the t4 and + # t5 blocks of the second reduction. + # + # Unlike R-interior, this block propagates carries through an existing fifth + # accumulator limb rather than terminating with a new carry limb. + # + # h0, l0 = bits.Mul64(t[k], orderComplementLimb0) + # h1, l1 = bits.Mul64(t[k], orderComplementLimb1) + # // h2, l2 = bits.Mul64(t[k], orderComplementLimb2) => h2=0, l2=t[k] + # t[j], carry = bits.Add64(t[j], l0, 0) + # t[j+1], carry = bits.Add64(t[j+1], h0, carry) + # ... + # t[j+4], _ = bits.Add64(t[j+4], 0, carry) + # + # d0..d4 model the incoming running total in t0..t4. + # + # d4 is zero when instantiated for the t4 block. + # + # The t4 additions are modeled as add64 with the carry-out assumed zero. + # This fact is proven above by the discard checks for the real totals, so + # this does not smuggle in a new assumption. + # ------------------------------------------------------------------------- + d = BitVecs("d0 d1 d2 d3 d4", 64) + w = [None]*5 + w[0], carry = add64(d[0], l0, ZERO) + w[1], carry = add64(d[1], h0, carry) + w[2], carry = add64(d[2], h1, carry) + w[3], carry = add64(d[3], ZERO, carry) + w[4], discarded1 = add64(d[4], ZERO, carry) + w[1], carry = add64(w[1], l1, ZERO) + w[2], carry = add64(w[2], x_j, carry) + w[3], carry = add64(w[3], ZERO, carry) + w[4], discarded2 = add64(w[4], ZERO, carry) + prove( + pack_limbs(w) == pack_limbs(d) + x_j_times_c, + "R-second-interior: block != state + xj*c", + assumptions=[discarded1 == ZERO, discarded2 == ZERO], + ) + + # ------------------------------------------------------------------------- + # Lemma R-second-tail: The t6 block of the second reduction. + # + # t2, carry = bits.Add64(t2, t6*orderComplementLimb0, 0) + # t3, carry = bits.Add64(t3, t6*orderComplementLimb1, carry) + # t4, _ = bits.Add64(t4, t6, carry) + # + # f0..f2 model the incoming running total in t2..t4. + # + # t6 models the incoming carry limb produced by the preceding block. + # + # The real implementation uses plain (truncating) 64-bit products here rather + # than bits.Mul64. Since t6 ≤ 1, these products are exact, so modeling them + # as ordinary 64-bit multiplications is equivalent to the implementation. + # + # Assumes t6 ≤ 1 and one discarded carry is 0. These facts are both proven + # above, so this is not smuggling in new assumptions. + # ------------------------------------------------------------------------- + f = BitVecs("f0 f1 f2", 64) + t6 = BitVec("t6", 64) + w = [None]*3 + w[0], carry = add64(f[0], t6*orderComplementLimb0, ZERO) + w[1], carry = add64(f[1], t6*orderComplementLimb1, carry) + w[2], discarded = add64(f[2], t6, carry) + t6_times_c = compose_mul_by_c( + ZeroExt(64, t6) * ZeroExt(64, orderComplementLimb0), + ZeroExt(64, t6) * ZeroExt(64, orderComplementLimb1), + t6, + ) + prove( + pack_limbs(w) == pack_limbs(f) + t6_times_c, + "R-second-tail: block != state + t6*c", + assumptions=[ULE(t6, 1), discarded == ZERO], + ) + + # ------------------------------------------------------------------------- + # Lemma R-third: The third reduction. + # + # t0, carry = bits.Add64(t0, t4*orderComplementLimb0, 0) + # t1, carry = bits.Add64(t1, t4*orderComplementLimb1, carry) + # t2, carry = bits.Add64(t2, t4, carry) + # t3, carry = bits.Add64(t3, 0, carry) + # t4 = carry + # + # g0..g3 model the incoming running total in t0..t3. Although the product + # is added only into t0..t2, the carry chain propagates through t3. + # + # t4 models the incoming carry limb produced by the preceding block. + # + # The real implementation uses plain (truncating) 64-bit products here rather + # than bits.Mul64. Since t4 ≤ 3, these products are exact, so modeling them + # as ordinary 64-bit multiplications is equivalent to the implementation. + # + # Assumes t4 ≤ 3. This fact is proven above, so this is not smuggling in + # a new assumption. + # ------------------------------------------------------------------------- + g = BitVecs("g0 g1 g2 g3", 64) + t4 = BitVec("t4", 64) + w = [None]*5 + w[0], carry = add64(g[0], t4*orderComplementLimb0, ZERO) + w[1], carry = add64(g[1], t4*orderComplementLimb1, carry) + w[2], carry = add64(g[2], t4, carry) + w[3], carry = add64(g[3], ZERO, carry) + w[4] = carry + t4_times_c = compose_mul_by_c( + ZeroExt(64, t4) * ZeroExt(64, orderComplementLimb0), + ZeroExt(64, t4) * ZeroExt(64, orderComplementLimb1), + t4, + ) + prove( + pack_limbs(w) == pack_limbs(g) + t4_times_c, + "R-third: 3rd reduction != state + t4*c", + assumptions=[ULE(t4, 3)], + ) + + # ------------------------------------------------------------------------- + # Lemma R-final: The conditional-subtraction final reduction. + # + # s0, borrow = bits.Sub64(t0, orderLimb0, 0) + # ... + # _, borrow = bits.Sub64(t4, 0, borrow) + # r[i] = constantTimeSelect64(borrow, t[i], s[i]) + # + # z0..z4 model t0..t4 going into this stage. + # + # This is only valid because t < 2N as otherwise t - N could still exceed N + # and the 4-limb result wouldn't represent it. This fact is proven above + # for the real total, so this does not smuggle in a new assumption. + # ------------------------------------------------------------------------- + n = cwidth(order) + z = BitVecs("z0 z1 z2 z3 z4", 64) + s = [None]*4 + s[0], borrow = sub64(z[0], orderLimb0, ZERO) + s[1], borrow = sub64(z[1], orderLimb1, borrow) + s[2], borrow = sub64(z[2], orderLimb2, borrow) + s[3], borrow = sub64(z[3], orderLimb3, borrow) + _, borrow = sub64(z[4], ZERO, borrow) + t = pack_limbs(z) + r = If(borrow == ONE, t, pack_limbs(s)) + prove( + r == If(ULT(t, n), t, t - n), + "R-final: conditional-subtract result != t mod N", + assumptions=[ULT(t, cwidth(twiceOrder))], + ) + +prove_functional_congruence_lemmas() \ No newline at end of file diff --git a/dcrec/secp256k1/modnscalar.go b/dcrec/secp256k1/modnscalar.go index 8c3f87c74d..f3e2df4eab 100644 --- a/dcrec/secp256k1/modnscalar.go +++ b/dcrec/secp256k1/modnscalar.go @@ -381,6 +381,10 @@ func (s *ModNScalar) Add(val *ModNScalar) *ModNScalar { // scalar64Reduce512 reduces a 512-bit little-endian limb array modulo the group // order in constant time and stores the result in r. func scalar64Reduce512(r *[4]uint64, x *[8]uint64) { + // This algorithm has been formally verified, including its intermediate + // bounds, carry assumptions, and functional correctness. The verification + // artifacts are available in internal/proofs. + // The overall strategy employed here is: // 1) Start with the full unreduced 512-bit product of the two scalars. // 2) Reduce the result modulo the group order via Crandall reduction From ac88d32c61500d6621a51a11918e509b2bee910a Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sun, 19 Jul 2026 08:06:40 -0500 Subject: [PATCH 07/33] secp256k1: Add width-5 windowed NAF recoder. With the ultimate goal of optimizing scalar multiplication by reducing the average number of point additions, this commit adds code to convert a balanced scalar representative into width-5 windowed non-adjacent form (wNAF). The current scalar multiplication implementation uses ordinary non-adjacent form (NAF), which corresponds to a window width of 2. This commit introduces only the width-5 wNAF recoder and does not yet integrate it into scalar multiplication. Future commits will add comprehensive tests and integrate the new implementation. --- dcrec/secp256k1/curve.go | 228 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) diff --git a/dcrec/secp256k1/curve.go b/dcrec/secp256k1/curve.go index 750b57a327..fe93eb8da6 100644 --- a/dcrec/secp256k1/curve.go +++ b/dcrec/secp256k1/curve.go @@ -943,6 +943,234 @@ func splitK(k *ModNScalar) (ModNScalar, ModNScalar) { return k1, k2 } +// wNAFWidth is the window width of the NAF representation used in scalar +// multiplication. +// +// It is important to note that this constant primarily exists to improve code +// readability, to parameterize parts of the scalar multiplication +// implementation, and to allow tests to assert invariants. +// +// However, the overall implementation is intentionally not fully parameterized +// based on this constant because it is carefully optimized for this specific +// window width and those optimizations involve taking advantage of known bounds +// that an arbitrary window size would violate. +// +// Choosing a new window size involves a variety of tradeoffs that must be +// carefully analyzed. For example, recoding costs, precomputation costs, and +// average density. +const wNAFWidth = 5 + +// wnaf5RecodeCodes maps the low five bits of an integer to the encoded +// representative of the width-5 wNAF digit that should be emitted. +// +// These values are not the signed digits themselves. Rather, each is an +// encoded representative that serves directly as an index into precomputed +// tables. See [wnafScalar] for the encoding details. +// +// For each odd residue, the represented signed digit is the unique value in +// {±1, ±3, ±5, ±7, ±9, ±11, ±13, ±15} that is congruent to the residue modulo +// 32: +// +// residue ≡ digit (mod 32) +// +// Thus, subtracting the represented signed digit always produces an integer +// that is divisible by 32. This allows the optimized recoding algorithm to +// skip five one-bit iterations of the canonical algorithm at once. +// +// Even residues always map to zero because they are already divisible by two +// and therefore correspond only to implicit zero digits. +// +// The corresponding arithmetic adjustment for each encoded representative is +// provided by [wnaf5RecodeAdjustments]. +var wnaf5RecodeCodes = [32]uint8{ + 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, + 0, 16, 0, 15, 0, 14, 0, 13, 0, 12, 0, 11, 0, 10, 0, 9, +} + +// wnaf5RecodeAdjustments maps the low five bits of an integer to the signed +// adjustment that must be added to the working integer before shifting during +// the width-5 wNAF recoding algorithm. +// +// While [wnaf5RecodeCodes] produces the encoded representative that is stored +// in the resulting wNAF encoding, this table provides the corresponding +// arithmetic adjustment. The represented signed digit d satisfies: +// +// residue ≡ d (mod 32) +// +// and the recoding algorithm updates the working integer via: +// +// n = (n - d) / 2 +// +// Rather than subtracting d directly, this table stores -d encoded as an +// unsigned 64-bit two's complement value. Negative adjustments therefore +// appear as values such as: +// +// -1 -> ^uint64(0) +// -3 -> ^uint64(2) +// -5 -> ^uint64(4) +// -7 -> ^uint64(6) +// -9 -> ^uint64(8) +// -11 -> ^uint64(10) +// -13 -> ^uint64(12) +// -15 -> ^uint64(14) +// +// The adjustment is sign-extended across the upper limbs during the multiword +// addition which allows the hot loop to remain branch free. +var wnaf5RecodeAdjustments = [32]uint64{ + 0, ^uint64(0), 0, ^uint64(2), 0, ^uint64(4), 0, ^uint64(6), + 0, ^uint64(8), 0, ^uint64(10), 0, ^uint64(12), 0, ^uint64(14), + 0, 15, 0, 13, 0, 11, 0, 9, 0, 7, 0, 5, 0, 3, 0, 1, +} + +// wnafScalar represents a non-negative integer less than 2^129 encoded in +// width-5 windowed non-adjacent form (wNAF). +// +// wNAF is a signed-digit representation where the valid digits are 0, ±1, ±3, +// ±5, ±7, ±9, ±11, ±13, and ±15. +// +// Each entry corresponds to one bit position and encodes the signed digit. +// +// The encoding from code to digit is: +// 0 -> 0 +// 1 -> +1 +// 2 -> +3 +// 3 -> +5 +// 4 -> +7 +// 5 -> +9 +// 6 -> +11 +// 7 -> +13 +// 8 -> +15 +// 9 -> -1 +// 10 -> -3 +// 11 -> -5 +// 12 -> -7 +// 13 -> -9 +// 14 -> -11 +// 15 -> -13 +// 16 -> -15 +// +// The encoding is intentionally not using a more compact form so that it serves +// directly as an index into precomputed tables which allows simplification of +// hot paths. +// +// Formally, letting c denote the stored code value, the decoded signed digit +// d(c) is given by the piecewise function: +// +// {0, where c = 0 +// d(c) = {2c - 1, where 0 < c ≤ 8 +// {17 - 2c, where 8 < c ≤ 16 +// +// The array contains one extra entry because a recoding may produce a carry +// into an additional most-significant digit. +type wnafScalar struct { + codes [130]uint8 + bits uint8 +} + +// wnaf takes a non-negative integer less than 2^129 and returns its width-5 +// windowed non-adjacent form (wNAF) which is a unique signed-digit +// representation such that non-zero digits are separated by at least 4 zeroes. +// See [wnafScalar] for details on how the representation is encoded and how to +// interpret it. +// +// Width-5 wNAF is useful because, on average, only about one in every six +// digits is non-zero. +// +// This property is particularly beneficial for optimizing elliptic curve point +// multiplication because it greatly reduces the number of point additions at +// the cost of precomputing a few odd multiples of the point and, in the worst +// case, one additional point doubling due to a carry introduced by the +// recoding. This is an excellent tradeoff because subtraction of points has +// the same computational complexity as addition of points and point doubling is +// faster than both. +func wnaf(k *ModNScalar) wnafScalar { + const ( + // This is intentionally not using the package constant [wNAFWidth] for + // the window size for the reasons stated by its documentation. + // + // In particular, this implementation is carefully optimized for this + // specific width and involves assumptions that an arbitrary window size + // might violate. + // + // Using a separate constant helps make it clear that updating the + // window size here requires extra care to assert correctness. + windowSize = 5 + windowMask = (1 << windowSize) - 1 + ) + + // The arithmetic for wNAF recoding is performed over the ordinary integers, + // not modulo the curve order. Moreover, this method is required to be + // called with the scalar's balanced integer representative, which is known + // to fit within 129 bits. + // + // Consequently, the scalar is first converted to a uint192 with three + // 64-bit limbs where the top limb is at most 1. + t0, t1, t2 := k.n[0], k.n[1], k.n[2] + + // This implementation is based on the standard width-w NAF recoding + // algorithm presented as Algorithm 3.35 in [GECC]. However, it has been + // modified to skip runs of zero digits instead of processing one bit at a + // time. + // + // The optimizations exploit the fact that runs of zero digits are implicit + // in the output representation. + // + // Also, note that the last non-zero bit is initialized to the maximum value + // so that adding 1 at the end to account for the exclusive endpoint wraps + // around to 0 when no digits are emitted. + var result wnafScalar + var c uint64 + var bit uint8 + var lastNonZeroBit = ^uint8(0) + for t0|t1|t2 != 0 { + // The next five iterations of the canonical bit-by-bit algorithm would + // emit only zero when the low window is zero, so skip directly to the + // next window boundary in that case. + if residue := uint8(t0 & windowMask); residue != 0 { + // Skip the zero digits that the canonical bit-by-bit algorithm + // would emit before reaching the next odd value. + // + // Since 0 < residue < 32, shift is guaranteed to satisfy the + // following bounds: + // + // 0 ≤ shift < 5 + shift := uint8(bits.TrailingZeros8(residue)) + t0 = t0>>shift | t1<<(64-shift) + t1 = t1>>shift | t2<<(64-shift) + t2 >>= shift + bit += shift + + residue = uint8(t0 & windowMask) + result.codes[bit] = wnaf5RecodeCodes[residue] + lastNonZeroBit = bit + + // The selected digit is congruent to the low five bits modulo 32. + // Therefore, adding the stored adjustment (which represents the + // negation of that digit) makes the value divisible by 32. + // + // Negative adjustments are stored in two's complement. Sign + // extending the value across the upper limbs allows the multiword + // addition to operate without branches. + adjustment := wnaf5RecodeAdjustments[residue] + signExtended := uint64(int64(adjustment) >> 63) + t0, c = bits.Add64(t0, adjustment, 0) + t1, c = bits.Add64(t1, signExtended, c) + t2, _ = bits.Add64(t2, signExtended, c) + } + + // Divide by 32 to prepare for the next window. + // + // The adjustment above guarantees the current value is divisible by 32. + t0 = t0>>windowSize | t1<<(64-windowSize) + t1 = t1>>windowSize | t2<<(64-windowSize) + t2 >>= windowSize + bit += windowSize + } + + result.bits = lastNonZeroBit + 1 + return result +} + // nafScalar represents a positive integer up to a maximum value of 2^256 - 1 // encoded in non-adjacent form. // From 2e39810eaec4ebc935da2e343706b5dd52bb1012 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sun, 19 Jul 2026 08:06:42 -0500 Subject: [PATCH 08/33] secp256k1: Add wNAF tests. This adds comprehensive tests for the new wNAF implementation to help verify correctness. The tests assert the resulting encoding satisfies the required mathematical properties and reconstructs the original value for both deterministic edge cases and randomized balanced scalar representatives. The deterministic edge cases exercise enough small values to exhaustively test every residue modulo 2^w multiple times, and include additional edge cases such as carries introduced by the recoding, maximum values, and alternating bit patterns. --- dcrec/secp256k1/curve_test.go | 172 ++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/dcrec/secp256k1/curve_test.go b/dcrec/secp256k1/curve_test.go index d3eb497f29..a92a6d1ca0 100644 --- a/dcrec/secp256k1/curve_test.go +++ b/dcrec/secp256k1/curve_test.go @@ -753,6 +753,178 @@ func TestNAFRandom(t *testing.T) { } } +// decodeWNAFDigit converts the provided wNAF code to the signed digit it +// represents. +func decodeWNAFDigit(code uint8) int8 { + const ( + positiveCodeLimit = 1 << (wNAFWidth - 2) + maxCode = 1 << (wNAFWidth - 1) + ) + switch { + case code == 0: + return 0 + case code <= positiveCodeLimit: + return 2*int8(code) - 1 + } + return (maxCode + 1) - 2*int8(code) +} + +// checkWNAFEncoding returns an error if the provided windowed NAF encoding does +// not adhere to the encoding requirements or does not reconstruct the provided +// original value. +func checkWNAFEncoding(s *wnafScalar, origValue *big.Int) error { + // Ensure the reported number of used bits does not exceed the size of the + // array. + if int(s.bits) > len(s.codes) { + return fmt.Errorf("bits %d > %d", s.bits, len(s.codes)) + } + + // wNAF must not have a leading zero and the individual codes must not + // exceed the max allowed code for the window width. + codes := s.codes[:s.bits] + if len(codes) > 0 && codes[len(codes)-1] == 0 { + return fmt.Errorf("leading zero in encoding: %v", codes) + } + const maxCode = 1 << (wNAFWidth - 1) + for _, code := range codes { + if code > maxCode { + return fmt.Errorf("found code %d > %d", code, maxCode) + } + } + + // Ensure each non-zero digit is separated by at least the width of the + // window. + const minDistance = wNAFWidth + prevNonZeroBit := -minDistance + for bit, code := range codes { + if code == 0 { + continue + } + + if distance := bit - prevNonZeroBit; distance < minDistance { + return fmt.Errorf("non-zero digits at bit pos %d and %d are only "+ + "%d bits apart", prevNonZeroBit, bit, distance) + } + + prevNonZeroBit = bit + } + + // Reconstruct the represented value and ensure it matches the original + // value. + sum := new(big.Int) + for bit, code := range codes { + digit := decodeWNAFDigit(code) + term := big.NewInt(int64(digit)) + term.Lsh(term, uint(bit)) + sum.Add(sum, term) + } + if origValue.Cmp(sum) != 0 { + return fmt.Errorf("failed to reconstruct orig value: got %v, want %v", + sum, origValue) + } + + return nil +} + +// TestWNAF ensures encoding various edge cases and values to width-w windowed +// non-adjacent form, where w is [wNAFWidth], produces valid results. +func TestWNAF(t *testing.T) { + type wnafTest struct { + name string // test description + in string // hex encoded test value + } + extraTests := []wnafTest{{ + name: "leading zeroes", + in: "002f20569b90697ad471c1be6107814f", + }, { + name: "highest allowed bit only", + in: "100000000000000000000000000000000", + }, { + name: "largest 129-bit value", + in: "1ffffffffffffffffffffffffffffffff", + }, { + name: "130 bits when NAF encoded", + in: "1f0000000000000000000000000000001", + }, { + name: "alternating bits (0xa5)", + in: "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", + }, { + name: "alternating bits (0x5a)", + in: "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", + }, { + name: "all ones", + in: "ffffffffffffffffffffffffffffffff", + }, { + name: "first term of balanced length-two representation #1", + in: "b776e53fb55f6b006a270d42d64ec2b1", + }, { + name: "second term balanced length-two representation #1", + in: "d6cc32c857f1174b604eefc544f0c7f7", + }, { + name: "first term of balanced length-two representation #2", + in: "45c53aa1bb56fcd68c011e2dad6758e4", + }, { + name: "second term of balanced length-two representation #2", + in: "a2e79d200f27f2360fba57619936159b", + }} + + // Add tests for the first small values since they collectively exercise + // every possible residue modulo 2^w, where w is the window width, multiple + // times. Then append the extra deterministic tests for edge cases. + const smallValues = 2*(1< Date: Sun, 19 Jul 2026 08:06:43 -0500 Subject: [PATCH 09/33] secp256k1: Add wNAF benchmark. --- dcrec/secp256k1/curve_bench_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dcrec/secp256k1/curve_bench_test.go b/dcrec/secp256k1/curve_bench_test.go index 5100ed8966..5bcd50e171 100644 --- a/dcrec/secp256k1/curve_bench_test.go +++ b/dcrec/secp256k1/curve_bench_test.go @@ -229,6 +229,18 @@ func BenchmarkNAF(b *testing.B) { } } +// BenchmarkWNAF benchmarks conversion of a non-negative integer into its +// width-w windowed non-adjacent form representation, where w is [wNAFWidth]. +func BenchmarkWNAF(b *testing.B) { + k := mustModNScalar("8447e288d34b360bc885cb8ce7c00575") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + wnaf(k) + } +} + // BenchmarkJacobianPointEquivalency benchmarks determining if two Jacobian // points represent the same affine point. func BenchmarkJacobianPointEquivalency(b *testing.B) { From f47e239301371221be6d71c21f19c4cb139698e5 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sun, 19 Jul 2026 08:06:45 -0500 Subject: [PATCH 10/33] secp256k1: Integrate width-5 wNAF in scalar mult. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This updates the non-constant-time scalar multiplication implementation to use the new width-5 wNAF recoding. In particular, because wNAF requires precomputed odd multiples that were not needed by the previous implementation, this introduces a new dedicated precomputed table type along with a new helper to construct the required multiples. The previous implementation only needed to swap between two precomputed points depending on the scalar signs. However, since the new wNAF implementation uses larger precomputed tables, it instead tracks whether each decomposed scalar was negated and applies the corresponding sign adjustment to the appropriate half of each table. The following benchmark shows a before and after comparison of scalar multiplication: name old time/op new time/op delta --------------------------------------------------------------------------- ScalarMultNonConst 94.4µs ± 1% 84.3µs ± 1% -10.75% (p=0.000 n=10+10) --- dcrec/secp256k1/curve.go | 243 ++++++++++++++++++++++++++------------- 1 file changed, 163 insertions(+), 80 deletions(-) diff --git a/dcrec/secp256k1/curve.go b/dcrec/secp256k1/curve.go index fe93eb8da6..1ce1f1bbf4 100644 --- a/dcrec/secp256k1/curve.go +++ b/dcrec/secp256k1/curve.go @@ -1307,6 +1307,111 @@ func naf(k []byte) nafScalar { return result } +// wNAFPrecompTableSize is the size of the wNAF precomputed point table. +const wNAFPrecompTableSize = 1 << (wNAFWidth - 1) + +// wNAFPrecompTable houses the precomputed odd point multiples used during wNAF +// scalar multiplication. +// +// The first half of the table (starting at index 1) contains the positive odd +// multiples: +// +// P, 3P, 5P, ... +// +// While the second half of the table contains the corresponding negatives: +// +// -P, -3P, -5P, ... +// +// Entry 0 is intentionally left unused so the encoded wNAF digit can be used +// directly as an array index. +type wNAFPrecompTable [wNAFPrecompTableSize + 1]JacobianPoint + +// wNAFNegateStart returns the index of the half of a precomputed table whose +// points must be negated in order to match the sign adjustment made to the +// corresponding scalar. +// +// When the scalar has been negated, the first half is negated so it represents +// the negative odd multiples while the second half remains positive. +// Otherwise, the opposite arrangement is used. +func wNAFNegateStart(isScalarNegated bool) int { + if isScalarNegated { + return 1 + } + return wNAFPrecompTableSize/2 + 1 +} + +// buildOddPrecomputeTables constructs the odd multiple tables for both P and +// φ(P) and arranges the positive and negative representatives so they can be +// indexed directly by encoded wNAF digits. +func buildOddPrecomputeTables(p *JacobianPoint, pTable, phiTable *wNAFPrecompTable, k1Neg, k2Neg bool) { + // oddMultiplesPerHalf is the total number of odd multiples in each half of + // the table. + const oddMultiplesPerHalf = wNAFPrecompTableSize / 2 + + // Build the positive odd multiples of P. + // + // Width-w wNAF only ever emits odd signed digits, so only odd multiples of + // the point need to be precomputed. + // + // Both positive and negative entries are ultimately needed. The second + // half is initially a copy of the positive half and the appropriate half is + // negated below depending on the sign of k1. + // + // The negation is deferred until after the φ(P) table is built because that + // table is constructed by applying φ to the corresponding positive odd + // multiples of P. + var twoP JacobianPoint + pTable[1].Set(p) + DoubleNonConst(&pTable[1], &twoP) + for i := 1; i < oddMultiplesPerHalf; i++ { + AddNonConst(&twoP, &pTable[i], &pTable[i+1]) + } + copy(pTable[oddMultiplesPerHalf+1:], pTable[1:oddMultiplesPerHalf+1]) + + // Build the corresponding odd multiples of φ(P). + // + // As above, both positive and negative entries are ultimately needed. The + // sign of this table is chosen independently from the P table because it + // depends only on the sign of k2. + // + // Note that φ is a group homomorphism, so: + // + // φ(n*P) = n*φ(P) + // + // This allows the second table to be obtained by applying φ to each entry + // of the first table instead of computing each odd multiple independently. + // + // NOTE: φ(x,y) = (βx,y). The Jacobian z coordinates are the same, so this + // math goes through. + for i := 1; i <= oddMultiplesPerHalf; i++ { + phiTable[i].Set(&pTable[i]) + phiTable[i].X.Mul(endoBeta).Normalize() + } + copy(phiTable[oddMultiplesPerHalf+1:], phiTable[1:oddMultiplesPerHalf+1]) + + // Negate whichever half of the table corresponds to the sign adjustment + // that was applied to k1. + // + // This maintains the invariant: + // + // k1*P = -k1*-P + negateStart := wNAFNegateStart(k1Neg) + for i := negateStart; i < negateStart+oddMultiplesPerHalf; i++ { + pTable[i].Y.Negate(1).Normalize() + } + + // Apply the same transformation independently to the φ(P) table based on + // the sign adjustment that was applied to k2. + // + // Similarly, this maintains the invariant: + // + // k2*φ(P) = -k2*-φ(P) + negateStart = wNAFNegateStart(k2Neg) + for i := negateStart; i < negateStart+oddMultiplesPerHalf; i++ { + phiTable[i].Y.Negate(1).Normalize() + } +} + // ScalarMultNonConst multiplies k*P where k is a scalar modulo the curve order // and P is a point in Jacobian projective coordinates and stores the result in // the provided Jacobian point. @@ -1354,25 +1459,6 @@ func ScalarMultNonConst(k *ModNScalar, point, result *JacobianPoint) { // See section 3.5 in [GECC] for a more rigorous treatment. // ------------------------------------------------------------------------- - // Per above, the main equation here to remember is: - // k*P = k1*P + k2*φ(P) - // - // p1 below is P in the equation while p2 is φ(P) in the equation. - // - // NOTE: φ(x,y) = (β*x,y). The Jacobian z coordinates are the same, so this - // math goes through. - // - // Also, calculate -p1 and -p2 for use in the NAF optimization. - p1, p1Neg := new(JacobianPoint), new(JacobianPoint) - p1.Set(point) - p1Neg.Set(p1) - p1Neg.Y.Negate(1).Normalize() - p2, p2Neg := new(JacobianPoint), new(JacobianPoint) - p2.Set(p1) - p2.X.Mul(endoBeta).Normalize() - p2Neg.Set(p2) - p2Neg.Y.Negate(1).Normalize() - // Decompose k into k1 and k2 such that k = k1 + k2*λ (mod n) where k1 and // k2 are around half the bit length of k in order to halve the number of EC // operations. @@ -1384,94 +1470,91 @@ func ScalarMultNonConst(k *ModNScalar, point, result *JacobianPoint) { // which means that when they would otherwise be a small negative magnitude // they will instead be a large positive magnitude. Since the goal is for // the scalars to have a small magnitude to achieve a performance boost, use - // their negation when they are greater than the half order of the group and - // flip the positive and negative values of the corresponding point that - // will be multiplied by to compensate. + // their negation when they are greater than the half order of the group. + // + // In order to compensate, the positive and negative values of the + // corresponding points that will be multiplied by are flipped later. // // In other words, transform the calc when k1 is over the half order to: // k1*P = -k1*-P // // Similarly, transform the calc when k2 is over the half order to: // k2*φ(P) = -k2*-φ(P) + var k1Neg, k2Neg bool k1, k2 := splitK(k) if k1.IsOverHalfOrder() { k1.Negate() - p1, p1Neg = p1Neg, p1 + k1Neg = true } if k2.IsOverHalfOrder() { k2.Negate() - p2, p2Neg = p2Neg, p2 + k2Neg = true } - // Convert k1 and k2 into their NAF representations since NAF has a lot more - // zeros overall on average which minimizes the number of required point - // additions in exchange for a mix of fewer point additions and subtractions - // at the cost of one additional point doubling. + // Per above, the main equation here to remember is: + // k*P = k1*P + k2*φ(P) + // + // The simultaneous multiplication therefore needs two sets of precomputed + // odd multiples: + // + // P, 3P, 5P, ... + // + // and: + // + // φ(P), 3φ(P), 5φ(P), ... + var pPrecomps, phiPrecomps wNAFPrecompTable + buildOddPrecomputeTables(point, &pPrecomps, &phiPrecomps, k1Neg, k2Neg) + + // Convert k1 and k2 into their windowed NAF representations since they have + // a lot more zeroes overall on average which greatly reduces the number of + // point additions at the cost of precomputing a few odd multiples of the + // point and, in the worst case, one additional point doubling due to a + // carry introduced by the recoding. // // This is an excellent tradeoff because subtraction of points has the same // computational complexity as addition of points and point doubling is // faster than both. // // Concretely, on average, 1/2 of all bits will be non-zero with the normal - // binary representation whereas only 1/3rd of the bits will be non-zero - // with NAF. - // - // The Pos version of the bytes contain the +1s and the Neg versions contain - // the -1s. - k1Bytes, k2Bytes := k1.Bytes(), k2.Bytes() - k1NAF, k2NAF := naf(k1Bytes[:]), naf(k2Bytes[:]) - k1PosNAF, k1NegNAF := k1NAF.Pos(), k1NAF.Neg() - k2PosNAF, k2NegNAF := k2NAF.Pos(), k2NAF.Neg() - k1Len, k2Len := len(k1PosNAF), len(k2PosNAF) + // binary representation whereas only 1/6 of the bits will be non-zero with + // width-5 wNAF. + k1NAF, k2NAF := wnaf(&k1), wnaf(&k2) - // Add left-to-right using the NAF optimization. See algorithm 3.77 from - // [GECC]. + // Add left-to-right using the endomorphism and wNAF optimizations. See + // algorithms 3.36 and 3.77 from [GECC]. // // Point Q = ∞ (point at infinity). + // + // Like ordinary binary scalar multiplication, the accumulator is doubled + // once per processed bit position. However, instead of each bit + // contributing either 0 or 1 copies of the point, each non-zero wNAF digit + // contributes a precomputed odd multiple while the preceding doublings + // supply the required power-of-two scaling. + // + // ±P, ±3P, ±5P, ... + // + // Since non-zero digits are sparse and separated by several zero digits, + // relatively few point additions are required compared to the ordinary + // binary representation. var q JacobianPoint - m := k1Len - if m < k2Len { - m = k2Len + maxBits := k1NAF.bits + if maxBits < k2NAF.bits { + maxBits = k2NAF.bits } - for i := 0; i < m; i++ { - // Since k1 and k2 are potentially different lengths and the calculation - // is being done left to right, pad the front of the shorter one with - // 0s. - var k1BytePos, k1ByteNeg, k2BytePos, k2ByteNeg byte - if i >= m-k1Len { - k1BytePos, k1ByteNeg = k1PosNAF[i-(m-k1Len)], k1NegNAF[i-(m-k1Len)] - } - if i >= m-k2Len { - k2BytePos, k2ByteNeg = k2PosNAF[i-(m-k2Len)], k2NegNAF[i-(m-k2Len)] + for bit := maxBits; bit > 0; bit-- { + // Q = 2 * Q + DoubleNonConst(&q, &q) + + // The encoded wNAF digit of k1 serves directly as an index into the + // precomputed odd multiples of P. + if code := k1NAF.codes[bit-1]; code != 0 { + AddNonConst(&q, &pPrecomps[code], &q) } - for mask := uint8(1 << 7); mask > 0; mask >>= 1 { - // Q = 2 * Q - DoubleNonConst(&q, &q) - - // Add or subtract the first point based on the signed digit of the - // NAF representation of k1 at this bit position. - // - // +1: Q = Q + p1 - // -1: Q = Q - p1 - // 0: Q = Q (no change) - if k1BytePos&mask == mask { - AddNonConst(&q, p1, &q) - } else if k1ByteNeg&mask == mask { - AddNonConst(&q, p1Neg, &q) - } - - // Add or subtract the second point based on the signed digit of the - // NAF representation of k2 at this bit position. - // - // +1: Q = Q + p2 - // -1: Q = Q - p2 - // 0: Q = Q (no change) - if k2BytePos&mask == mask { - AddNonConst(&q, p2, &q) - } else if k2ByteNeg&mask == mask { - AddNonConst(&q, p2Neg, &q) - } + // Likewise, the encoded wNAF digit of k2 serves directly as an index + // into the precomputed odd multiples of φ(P). + if code := k2NAF.codes[bit-1]; code != 0 { + AddNonConst(&q, &phiPrecomps[code], &q) } } From 155ec1becf95a6c26ecbaef0992af4a5cb8f2f08 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sun, 19 Jul 2026 08:49:14 -0500 Subject: [PATCH 11/33] secp256k1: Remove unused NAF code. This removes the old ordinary NAF implementation that is no longer used now that the implementation has been updated to use a new width-5 wNAF recoder instead. It also removes the associated tests and benchmark accordingly. --- dcrec/secp256k1/curve.go | 136 ---------------------------- dcrec/secp256k1/curve_bench_test.go | 13 --- dcrec/secp256k1/curve_test.go | 119 ------------------------ 3 files changed, 268 deletions(-) diff --git a/dcrec/secp256k1/curve.go b/dcrec/secp256k1/curve.go index 1ce1f1bbf4..fb9da3d265 100644 --- a/dcrec/secp256k1/curve.go +++ b/dcrec/secp256k1/curve.go @@ -1171,142 +1171,6 @@ func wnaf(k *ModNScalar) wnafScalar { return result } -// nafScalar represents a positive integer up to a maximum value of 2^256 - 1 -// encoded in non-adjacent form. -// -// NAF is a signed-digit representation where each digit can be +1, 0, or -1. -// -// In order to efficiently encode that information, this type uses two arrays, a -// "positive" array where set bits represent the +1 signed digits and a -// "negative" array where set bits represent the -1 signed digits. 0 is -// represented by neither array having a bit set in that position. -// -// The Pos and Neg methods return the aforementioned positive and negative -// arrays, respectively. -type nafScalar struct { - // pos houses the positive portion of the representation. An additional - // byte is required for the positive portion because the NAF encoding can be - // up to 1 bit longer than the normal binary encoding of the value. - // - // neg houses the negative portion of the representation. Even though the - // additional byte is not required for the negative portion, since it can - // never exceed the length of the normal binary encoding of the value, - // keeping the same length for positive and negative portions simplifies - // working with the representation and allows extra conditional branches to - // be avoided. - // - // start and end specify the starting and ending index to use within the pos - // and neg arrays, respectively. This allows fixed size arrays to be used - // versus needing to dynamically allocate space on the heap. - // - // NOTE: The fields are defined in the order that they are to minimize the - // padding on 32-bit and 64-bit platforms. - pos [33]byte - start, end uint8 - neg [33]byte -} - -// Pos returns the bytes of the encoded value with bits set in the positions -// that represent a signed digit of +1. -func (s *nafScalar) Pos() []byte { - return s.pos[s.start:s.end] -} - -// Neg returns the bytes of the encoded value with bits set in the positions -// that represent a signed digit of -1. -func (s *nafScalar) Neg() []byte { - return s.neg[s.start:s.end] -} - -// naf takes a positive integer up to a maximum value of 2^256 - 1 and returns -// its non-adjacent form (NAF), which is a unique signed-digit representation -// such that no two consecutive digits are nonzero. See the documentation for -// the returned type for details on how the representation is encoded -// efficiently and how to interpret it -// -// NAF is useful in that it has the fewest nonzero digits of any signed digit -// representation, only 1/3rd of its digits are nonzero on average, and at least -// half of the digits will be 0. -// -// The aforementioned properties are particularly beneficial for optimizing -// elliptic curve point multiplication because they effectively minimize the -// number of required point additions in exchange for needing to perform a mix -// of fewer point additions and subtractions and possibly one additional point -// doubling. This is an excellent tradeoff because subtraction of points has -// the same computational complexity as addition of points and point doubling is -// faster than both. -func naf(k []byte) nafScalar { - // Strip leading zero bytes. - for len(k) > 0 && k[0] == 0x00 { - k = k[1:] - } - - // The non-adjacent form (NAF) of a positive integer k is an expression - // k = ∑_(i=0, l-1) k_i * 2^i where k_i ∈ {0,±1}, k_(l-1) != 0, and no two - // consecutive digits k_i are nonzero. - // - // The traditional method of computing the NAF of a positive integer is - // given by algorithm 3.30 in [GECC]. It consists of repeatedly dividing k - // by 2 and choosing the remainder so that the quotient (k−r)/2 is even - // which ensures the next NAF digit is 0. This requires log_2(k) steps. - // - // However, in [BRID], Prodinger notes that a closed form expression for the - // NAF representation is the bitwise difference 3k/2 - k/2. This is more - // efficient as it can be computed in O(1) versus the O(log(n)) of the - // traditional approach. - // - // The following code makes use of that formula to compute the NAF more - // efficiently. - // - // To understand the logic here, observe that the only way the NAF has a - // nonzero digit at a given bit is when either 3k/2 or k/2 has a bit set in - // that position, but not both. In other words, the result of a bitwise - // xor. This can be seen simply by considering that when the bits are the - // same, the subtraction is either 0-0 or 1-1, both of which are 0. - // - // Further, observe that the "+1" digits in the result are contributed by - // 3k/2 while the "-1" digits are from k/2. So, they can be determined by - // taking the bitwise and of each respective value with the result of the - // xor which identifies which bits are nonzero. - // - // Using that information, this loops backwards from the least significant - // byte to the most significant byte while performing the aforementioned - // calculations by propagating the potential carry and high order bit from - // the next word during the right shift. - kLen := len(k) - var result nafScalar - var carry uint8 - for byteNum := kLen - 1; byteNum >= 0; byteNum-- { - // Calculate k/2. Notice the carry from the previous word is added and - // the low order bit from the next word is shifted in accordingly. - kc := uint16(k[byteNum]) + uint16(carry) - var nextWord uint8 - if byteNum > 0 { - nextWord = k[byteNum-1] - } - halfK := kc>>1 | uint16(nextWord<<7) - - // Calculate 3k/2 and determine the non-zero digits in the result. - threeHalfK := kc + halfK - nonZeroResultDigits := threeHalfK ^ halfK - - // Determine the signed digits {0, ±1}. - result.pos[byteNum+1] = uint8(threeHalfK & nonZeroResultDigits) - result.neg[byteNum+1] = uint8(halfK & nonZeroResultDigits) - - // Propagate the potential carry from the 3k/2 calculation. - carry = uint8(threeHalfK >> 8) - } - result.pos[0] = carry - - // Set the starting and ending positions within the fixed size arrays to - // identify the bytes that are actually used. This is important since the - // encoding is big endian and thus trailing zero bytes changes its value. - result.start = 1 - carry - result.end = uint8(kLen + 1) - return result -} - // wNAFPrecompTableSize is the size of the wNAF precomputed point table. const wNAFPrecompTableSize = 1 << (wNAFWidth - 1) diff --git a/dcrec/secp256k1/curve_bench_test.go b/dcrec/secp256k1/curve_bench_test.go index 5bcd50e171..7d8bf3089a 100644 --- a/dcrec/secp256k1/curve_bench_test.go +++ b/dcrec/secp256k1/curve_bench_test.go @@ -216,19 +216,6 @@ func BenchmarkScalarMultNonConst(b *testing.B) { } } -// BenchmarkNAF benchmarks conversion of a positive integer into its -// non-adjacent form representation. -func BenchmarkNAF(b *testing.B) { - k := fromHex("d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575") - kBytes := k.Bytes() - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - naf(kBytes) - } -} - // BenchmarkWNAF benchmarks conversion of a non-negative integer into its // width-w windowed non-adjacent form representation, where w is [wNAFWidth]. func BenchmarkWNAF(b *testing.B) { diff --git a/dcrec/secp256k1/curve_test.go b/dcrec/secp256k1/curve_test.go index a92a6d1ca0..3f32a39a4e 100644 --- a/dcrec/secp256k1/curve_test.go +++ b/dcrec/secp256k1/curve_test.go @@ -634,125 +634,6 @@ func TestDoubleJacobian(t *testing.T) { } } -// checkNAFEncoding returns an error if the provided positive and negative -// portions of an overall NAF encoding do not adhere to the requirements or they -// do not sum back to the provided original value. -func checkNAFEncoding(pos, neg []byte, origValue *big.Int) error { - // NAF must not have a leading zero byte and the number of negative - // bytes must not exceed the positive portion. - if len(pos) > 0 && pos[0] == 0 { - return fmt.Errorf("positive has leading zero -- got %x", pos) - } - if len(neg) > len(pos) { - return fmt.Errorf("negative has len %d > pos len %d", len(neg), - len(pos)) - } - - // Ensure the result doesn't have any adjacent non-zero digits. - gotPos := new(big.Int).SetBytes(pos) - gotNeg := new(big.Int).SetBytes(neg) - posOrNeg := new(big.Int).Or(gotPos, gotNeg) - prevBit := posOrNeg.Bit(0) - for bit := 1; bit < posOrNeg.BitLen(); bit++ { - thisBit := posOrNeg.Bit(bit) - if prevBit == 1 && thisBit == 1 { - return fmt.Errorf("adjacent non-zero digits found at bit pos %d", - bit-1) - } - prevBit = thisBit - } - - // Ensure the resulting positive and negative portions of the overall - // NAF representation sum back to the original value. - gotValue := new(big.Int).Sub(gotPos, gotNeg) - if origValue.Cmp(gotValue) != 0 { - return fmt.Errorf("pos-neg is not original value: got %x, want %x", - gotValue, origValue) - } - - return nil -} - -// TestNAF ensures encoding various edge cases and values to non-adjacent form -// produces valid results. -func TestNAF(t *testing.T) { - tests := []struct { - name string // test description - in string // hex encoded test value - }{{ - name: "empty is zero", - in: "", - }, { - name: "zero", - in: "00", - }, { - name: "just before first carry", - in: "aa", - }, { - name: "first carry", - in: "ab", - }, { - name: "leading zeroes", - in: "002f20569b90697ad471c1be6107814f53f47446be298a3a2a6b686b97d35cf9", - }, { - name: "257 bits when NAF encoded", - in: "c000000000000000000000000000000000000000000000000000000000000001", - }, { - name: "32-byte scalar", - in: "6df2b5d30854069ccdec40ae022f5c948936324a4e9ebed8eb82cfd5a6b6d766", - }, { - name: "first term of balanced length-two representation #1", - in: "b776e53fb55f6b006a270d42d64ec2b1", - }, { - name: "second term balanced length-two representation #1", - in: "d6cc32c857f1174b604eefc544f0c7f7", - }, { - name: "first term of balanced length-two representation #2", - in: "45c53aa1bb56fcd68c011e2dad6758e4", - }, { - name: "second term of balanced length-two representation #2", - in: "a2e79d200f27f2360fba57619936159b", - }} - - for _, test := range tests { - // Ensure the resulting positive and negative portions of the overall - // NAF representation adhere to the requirements of NAF encoding and - // they sum back to the original value. - result := naf(hexToBytes(test.in)) - pos, neg := result.Pos(), result.Neg() - if err := checkNAFEncoding(pos, neg, fromHex(test.in)); err != nil { - t.Errorf("%q: %v", test.name, err) - } - } -} - -// TestNAFRandom ensures that encoding randomly-generated values to non-adjacent -// form produces valid results. -func TestNAFRandom(t *testing.T) { - // Use a unique random seed each test instance and log it if the tests fail. - seed := time.Now().Unix() - rng := mrand.New(mrand.NewSource(seed)) - defer func(t *testing.T, seed int64) { - if t.Failed() { - t.Logf("random seed: %d", seed) - } - }(t, seed) - - for i := 0; i < 100; i++ { - // Ensure the resulting positive and negative portions of the overall - // NAF representation adhere to the requirements of NAF encoding and - // they sum back to the original value. - bigIntVal, modNVal := randIntAndModNScalar(t, rng) - valBytes := modNVal.Bytes() - result := naf(valBytes[:]) - pos, neg := result.Pos(), result.Neg() - if err := checkNAFEncoding(pos, neg, bigIntVal); err != nil { - t.Fatalf("encoding err: %v\nin: %x\npos: %x\nneg: %x", err, - bigIntVal, pos, neg) - } - } -} - // decodeWNAFDigit converts the provided wNAF code to the signed digit it // represents. func decodeWNAFDigit(code uint8) int8 { From 7d198f28c4b2db7d2596088157e7a11201c213f0 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sat, 25 Jul 2026 04:01:51 -0500 Subject: [PATCH 12/33] secp256k1: Move constant time helpers to arith. The package currently defines several generic constant time helpers in the main package despite them not being specific to any particular type or algorithm. They are instead arithmetic primitives that are shared by multiple implementations. Notably, the helpers used by the field implementations that will be split into separate packages in future commits to prepare for architecture-specific backend selection. In order to smooth that transition, this moves the helpers into a new internal arith package and updates all call sites to use them. No functional changes are intended. The move is purely organizational and preserves the existing constant time semantics. --- dcrec/secp256k1/common.go | 56 ------------- dcrec/secp256k1/field.go | 82 ++++++++++--------- dcrec/secp256k1/field64.go | 32 ++++---- dcrec/secp256k1/internal/arith/consttime.go | 56 +++++++++++++ .../arith/consttime_test.go} | 4 +- dcrec/secp256k1/modnscalar.go | 33 ++++---- 6 files changed, 135 insertions(+), 128 deletions(-) delete mode 100644 dcrec/secp256k1/common.go create mode 100644 dcrec/secp256k1/internal/arith/consttime.go rename dcrec/secp256k1/{common_test.go => internal/arith/consttime_test.go} (93%) diff --git a/dcrec/secp256k1/common.go b/dcrec/secp256k1/common.go deleted file mode 100644 index 9ff782bf40..0000000000 --- a/dcrec/secp256k1/common.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2020-2026 The Decred developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package secp256k1 - -// constantTimeEq returns 1 if a == b or 0 otherwise in constant time. -func constantTimeEq(a, b uint32) uint32 { - return uint32((uint64(a^b) - 1) >> 63) -} - -// constantTimeEq64 returns 1 if a == b or 0 otherwise in constant time. -func constantTimeEq64(a, b uint64) uint32 { - t := a ^ b - return uint32(((t | -t) >> 63) ^ 1) -} - -// constantTimeNotEq64 returns 1 if a != b or 0 otherwise in constant time. -func constantTimeNotEq64(a, b uint64) uint32 { - t := a ^ b - return uint32(((t | -t) >> 63)) -} - -// constantTimeLess returns 1 if a < b or 0 otherwise in constant time. -func constantTimeLess(a, b uint32) uint32 { - return uint32((uint64(a) - uint64(b)) >> 63) -} - -// constantTimeLessOrEq returns 1 if a <= b or 0 otherwise in constant time. -func constantTimeLessOrEq(a, b uint32) uint32 { - return uint32((uint64(a) - uint64(b) - 1) >> 63) -} - -// constantTimeGreater returns 1 if a > b or 0 otherwise in constant time. -func constantTimeGreater(a, b uint32) uint32 { - return constantTimeLess(b, a) -} - -// constantTimeGreaterOrEq returns 1 if a >= b or 0 otherwise in constant time. -func constantTimeGreaterOrEq(a, b uint32) uint32 { - return constantTimeLessOrEq(b, a) -} - -// constantTimeMin returns min(a,b) in constant time. -func constantTimeMin(a, b uint32) uint32 { - return b ^ ((a ^ b) & -constantTimeLess(a, b)) -} - -// constantTimeSelect64 returns a if cond == 1 or b if cond == 0 in constant -// time. -// -// WARNING: The behavior is undefined if cond is anything other than 0 or 1. -func constantTimeSelect64(cond, a, b uint64) uint64 { - mask := -cond - return b ^ (a^b)&mask -} diff --git a/dcrec/secp256k1/field.go b/dcrec/secp256k1/field.go index 9c7c9367f0..76a9c48742 100644 --- a/dcrec/secp256k1/field.go +++ b/dcrec/secp256k1/field.go @@ -6,6 +6,12 @@ package secp256k1 +import ( + "encoding/hex" + + "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/arith" +) + // References: // [HAC]: Handbook of Applied Cryptography Menezes, van Oorschot, Vanstone. // https://cacr.uwaterloo.ca/hac/ @@ -59,10 +65,6 @@ package secp256k1 // performance crypto, this type does not perform any validation where it // ordinarily would. See the documentation for [FieldVal] for more details. -import ( - "encoding/hex" -) - // Constants used to make the code more readable. const ( twoBitsMask = 0x3 @@ -283,17 +285,17 @@ func (f *FieldVal) SetBytes(b *[32]byte) uint32 { // // Thus, there is no need to test if the upper words of the field value // exceeds them, hence, only equality is checked for them. - highWordsEq := constantTimeEq(f.n[9], fieldPrimeWordNine) - highWordsEq &= constantTimeEq(f.n[8], fieldPrimeWordEight) - highWordsEq &= constantTimeEq(f.n[7], fieldPrimeWordSeven) - highWordsEq &= constantTimeEq(f.n[6], fieldPrimeWordSix) - highWordsEq &= constantTimeEq(f.n[5], fieldPrimeWordFive) - highWordsEq &= constantTimeEq(f.n[4], fieldPrimeWordFour) - highWordsEq &= constantTimeEq(f.n[3], fieldPrimeWordThree) - highWordsEq &= constantTimeEq(f.n[2], fieldPrimeWordTwo) - overflow := highWordsEq & constantTimeGreater(f.n[1], fieldPrimeWordOne) - highWordsEq &= constantTimeEq(f.n[1], fieldPrimeWordOne) - overflow |= highWordsEq & constantTimeGreaterOrEq(f.n[0], fieldPrimeWordZero) + highWordsEq := arith.ConstantTimeEq(f.n[9], fieldPrimeWordNine) + highWordsEq &= arith.ConstantTimeEq(f.n[8], fieldPrimeWordEight) + highWordsEq &= arith.ConstantTimeEq(f.n[7], fieldPrimeWordSeven) + highWordsEq &= arith.ConstantTimeEq(f.n[6], fieldPrimeWordSix) + highWordsEq &= arith.ConstantTimeEq(f.n[5], fieldPrimeWordFive) + highWordsEq &= arith.ConstantTimeEq(f.n[4], fieldPrimeWordFour) + highWordsEq &= arith.ConstantTimeEq(f.n[3], fieldPrimeWordThree) + highWordsEq &= arith.ConstantTimeEq(f.n[2], fieldPrimeWordTwo) + overflow := highWordsEq & arith.ConstantTimeGreater(f.n[1], fieldPrimeWordOne) + highWordsEq &= arith.ConstantTimeEq(f.n[1], fieldPrimeWordOne) + overflow |= highWordsEq & arith.ConstantTimeGreaterOrEq(f.n[0], fieldPrimeWordZero) return overflow } @@ -316,7 +318,7 @@ func (f *FieldVal) SetBytes(b *[32]byte) uint32 { // Output Max Magnitude: 1 func (f *FieldVal) SetByteSlice(b []byte) bool { var b32 [32]byte - b = b[:constantTimeMin(uint32(len(b)), 32)] + b = b[:arith.ConstantTimeMin(uint32(len(b)), 32)] copy(b32[:], b32[:32-len(b)]) copy(b32[32-len(b):], b) result := f.SetBytes(&b32) @@ -393,9 +395,9 @@ func (f *FieldVal) Normalize() *FieldVal { // // Also note that 'm' will be zero when neither of the aforementioned // conditions are true and the value will not be changed when 'm' is zero. - m = constantTimeEq(t9, fieldMSBMask) - m &= constantTimeEq(t8&t7&t6&t5&t4&t3&t2, fieldBaseMask) - m &= constantTimeGreater(t1+64+((t0+977)>>fieldBase), fieldBaseMask) + m = arith.ConstantTimeEq(t9, fieldMSBMask) + m &= arith.ConstantTimeEq(t8&t7&t6&t5&t4&t3&t2, fieldBaseMask) + m &= arith.ConstantTimeGreater(t1+64+((t0+977)>>fieldBase), fieldBaseMask) m |= t9 >> fieldMSBBits t0 += m * 977 t1 = (t0 >> fieldBase) + t1 + (m << 6) @@ -532,7 +534,7 @@ func (f *FieldVal) IsZeroBit() uint32 { bits := f.n[0] | f.n[1] | f.n[2] | f.n[3] | f.n[4] | f.n[5] | f.n[6] | f.n[7] | f.n[8] | f.n[9] - return constantTimeEq(bits, 0) + return arith.ConstantTimeEq(bits, 0) } // IsZero returns whether or not the field value is equal to zero in constant @@ -566,7 +568,7 @@ func (f *FieldVal) IsOneBit() uint32 { bits := (f.n[0] ^ 1) | f.n[1] | f.n[2] | f.n[3] | f.n[4] | f.n[5] | f.n[6] | f.n[7] | f.n[8] | f.n[9] - return constantTimeEq(bits, 0) + return arith.ConstantTimeEq(bits, 0) } // IsOne returns whether or not the field value is equal to one in constant @@ -1741,25 +1743,25 @@ func (f *FieldVal) IsGtOrEqPrimeMinusOrder() bool { // The intuition here is that the value is greater than field prime minus // the group order if one of the higher individual words is greater than the // corresponding word and all higher words in the value are equal. - result := constantTimeGreater(f.n[9], pMinusNWordNine) - highWordsEqual := constantTimeEq(f.n[9], pMinusNWordNine) - result |= highWordsEqual & constantTimeGreater(f.n[8], pMinusNWordEight) - highWordsEqual &= constantTimeEq(f.n[8], pMinusNWordEight) - result |= highWordsEqual & constantTimeGreater(f.n[7], pMinusNWordSeven) - highWordsEqual &= constantTimeEq(f.n[7], pMinusNWordSeven) - result |= highWordsEqual & constantTimeGreater(f.n[6], pMinusNWordSix) - highWordsEqual &= constantTimeEq(f.n[6], pMinusNWordSix) - result |= highWordsEqual & constantTimeGreater(f.n[5], pMinusNWordFive) - highWordsEqual &= constantTimeEq(f.n[5], pMinusNWordFive) - result |= highWordsEqual & constantTimeGreater(f.n[4], pMinusNWordFour) - highWordsEqual &= constantTimeEq(f.n[4], pMinusNWordFour) - result |= highWordsEqual & constantTimeGreater(f.n[3], pMinusNWordThree) - highWordsEqual &= constantTimeEq(f.n[3], pMinusNWordThree) - result |= highWordsEqual & constantTimeGreater(f.n[2], pMinusNWordTwo) - highWordsEqual &= constantTimeEq(f.n[2], pMinusNWordTwo) - result |= highWordsEqual & constantTimeGreater(f.n[1], pMinusNWordOne) - highWordsEqual &= constantTimeEq(f.n[1], pMinusNWordOne) - result |= highWordsEqual & constantTimeGreaterOrEq(f.n[0], pMinusNWordZero) + result := arith.ConstantTimeGreater(f.n[9], pMinusNWordNine) + highWordsEqual := arith.ConstantTimeEq(f.n[9], pMinusNWordNine) + result |= highWordsEqual & arith.ConstantTimeGreater(f.n[8], pMinusNWordEight) + highWordsEqual &= arith.ConstantTimeEq(f.n[8], pMinusNWordEight) + result |= highWordsEqual & arith.ConstantTimeGreater(f.n[7], pMinusNWordSeven) + highWordsEqual &= arith.ConstantTimeEq(f.n[7], pMinusNWordSeven) + result |= highWordsEqual & arith.ConstantTimeGreater(f.n[6], pMinusNWordSix) + highWordsEqual &= arith.ConstantTimeEq(f.n[6], pMinusNWordSix) + result |= highWordsEqual & arith.ConstantTimeGreater(f.n[5], pMinusNWordFive) + highWordsEqual &= arith.ConstantTimeEq(f.n[5], pMinusNWordFive) + result |= highWordsEqual & arith.ConstantTimeGreater(f.n[4], pMinusNWordFour) + highWordsEqual &= arith.ConstantTimeEq(f.n[4], pMinusNWordFour) + result |= highWordsEqual & arith.ConstantTimeGreater(f.n[3], pMinusNWordThree) + highWordsEqual &= arith.ConstantTimeEq(f.n[3], pMinusNWordThree) + result |= highWordsEqual & arith.ConstantTimeGreater(f.n[2], pMinusNWordTwo) + highWordsEqual &= arith.ConstantTimeEq(f.n[2], pMinusNWordTwo) + result |= highWordsEqual & arith.ConstantTimeGreater(f.n[1], pMinusNWordOne) + highWordsEqual &= arith.ConstantTimeEq(f.n[1], pMinusNWordOne) + result |= highWordsEqual & arith.ConstantTimeGreaterOrEq(f.n[0], pMinusNWordZero) return result != 0 } diff --git a/dcrec/secp256k1/field64.go b/dcrec/secp256k1/field64.go index 5be951eb0b..30afab1e2b 100644 --- a/dcrec/secp256k1/field64.go +++ b/dcrec/secp256k1/field64.go @@ -8,6 +8,8 @@ import ( "encoding/binary" "encoding/hex" "math/bits" + + "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/arith" ) // References: @@ -137,10 +139,10 @@ func (f *FieldVal64) SetBytes(b *[32]byte) uint32 { // Constant-time select. // // Set f = f when f < p (aka borrow is set). Otherwise f = s = f - p. - f.n[0] = constantTimeSelect64(borrow, f.n[0], s0) - f.n[1] = constantTimeSelect64(borrow, f.n[1], s1) - f.n[2] = constantTimeSelect64(borrow, f.n[2], s2) - f.n[3] = constantTimeSelect64(borrow, f.n[3], s3) + f.n[0] = arith.ConstantTimeSelect64(borrow, f.n[0], s0) + f.n[1] = arith.ConstantTimeSelect64(borrow, f.n[1], s1) + f.n[2] = arith.ConstantTimeSelect64(borrow, f.n[2], s2) + f.n[3] = arith.ConstantTimeSelect64(borrow, f.n[3], s3) return uint32(1 - borrow) } @@ -158,7 +160,7 @@ func (f *FieldVal64) SetBytes(b *[32]byte) uint32 { // overflow behavior. func (f *FieldVal64) SetByteSlice(b []byte) bool { var b32 [32]byte - b = b[:constantTimeMin(uint32(len(b)), 32)] + b = b[:arith.ConstantTimeMin(uint32(len(b)), 32)] copy(b32[:], b32[:32-len(b)]) copy(b32[32-len(b):], b) result := f.SetBytes(&b32) @@ -220,7 +222,7 @@ func (f *FieldVal64) Bytes() *[32]byte { // operations require a numeric value. See IsZero for the version that returns // a bool. func (f *FieldVal64) IsZeroBit() uint32 { - return constantTimeEq64(f.n[0]|f.n[1]|f.n[2]|f.n[3], 0) + return arith.ConstantTimeEq64(f.n[0]|f.n[1]|f.n[2]|f.n[3], 0) } // IsZero returns whether or not the field value is equal to zero in constant @@ -240,7 +242,7 @@ func (f *FieldVal64) IsOneBit() uint32 { // The value can only be one if the single lowest significant bit is set in // the first word and no other bits are set in any of the other words. // This is a constant time implementation. - return constantTimeEq64((f.n[0]^1)|f.n[1]|f.n[2]|f.n[3], 0) + return arith.ConstantTimeEq64((f.n[0]^1)|f.n[1]|f.n[2]|f.n[3], 0) } // IsOne returns whether or not the field value is equal to one in constant @@ -389,10 +391,10 @@ func (f *FieldVal64) Add2(a, b *FieldVal64) *FieldVal64 { // Set f = t = a+b only when there was no overflow and t < p (borrow set). // Otherwise f = s = a+b - p. cond := (1 - overflow) & borrow - f.n[0] = constantTimeSelect64(cond, t0, s0) - f.n[1] = constantTimeSelect64(cond, t1, s1) - f.n[2] = constantTimeSelect64(cond, t2, s2) - f.n[3] = constantTimeSelect64(cond, t3, s3) + f.n[0] = arith.ConstantTimeSelect64(cond, t0, s0) + f.n[1] = arith.ConstantTimeSelect64(cond, t1, s1) + f.n[2] = arith.ConstantTimeSelect64(cond, t2, s2) + f.n[3] = arith.ConstantTimeSelect64(cond, t3, s3) return f } @@ -943,10 +945,10 @@ func field64Reduce512(r *[4]uint64, x *[8]uint64) { s2, borrow = bits.Sub64(t2, field64Prime2, borrow) s3, borrow = bits.Sub64(t3, field64Prime3, borrow) _, borrow = bits.Sub64(t4, 0, borrow) - r[0] = constantTimeSelect64(borrow, t0, s0) - r[1] = constantTimeSelect64(borrow, t1, s1) - r[2] = constantTimeSelect64(borrow, t2, s2) - r[3] = constantTimeSelect64(borrow, t3, s3) + r[0] = arith.ConstantTimeSelect64(borrow, t0, s0) + r[1] = arith.ConstantTimeSelect64(borrow, t1, s1) + r[2] = arith.ConstantTimeSelect64(borrow, t2, s2) + r[3] = arith.ConstantTimeSelect64(borrow, t3, s3) } // Inverse finds the modular multiplicative inverse of the field value in diff --git a/dcrec/secp256k1/internal/arith/consttime.go b/dcrec/secp256k1/internal/arith/consttime.go new file mode 100644 index 0000000000..b798e824d5 --- /dev/null +++ b/dcrec/secp256k1/internal/arith/consttime.go @@ -0,0 +1,56 @@ +// Copyright (c) 2020-2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package arith + +// ConstantTimeEq returns 1 if a == b or 0 otherwise in constant time. +func ConstantTimeEq(a, b uint32) uint32 { + return uint32((uint64(a^b) - 1) >> 63) +} + +// ConstantTimeEq64 returns 1 if a == b or 0 otherwise in constant time. +func ConstantTimeEq64(a, b uint64) uint32 { + t := a ^ b + return uint32(((t | -t) >> 63) ^ 1) +} + +// ConstantTimeNotEq64 returns 1 if a != b or 0 otherwise in constant time. +func ConstantTimeNotEq64(a, b uint64) uint32 { + t := a ^ b + return uint32((t | -t) >> 63) +} + +// ConstantTimeLess returns 1 if a < b or 0 otherwise in constant time. +func ConstantTimeLess(a, b uint32) uint32 { + return uint32((uint64(a) - uint64(b)) >> 63) +} + +// ConstantTimeLessOrEq returns 1 if a <= b or 0 otherwise in constant time. +func ConstantTimeLessOrEq(a, b uint32) uint32 { + return uint32((uint64(a) - uint64(b) - 1) >> 63) +} + +// ConstantTimeGreater returns 1 if a > b or 0 otherwise in constant time. +func ConstantTimeGreater(a, b uint32) uint32 { + return ConstantTimeLess(b, a) +} + +// ConstantTimeGreaterOrEq returns 1 if a >= b or 0 otherwise in constant time. +func ConstantTimeGreaterOrEq(a, b uint32) uint32 { + return ConstantTimeLessOrEq(b, a) +} + +// ConstantTimeMin returns min(a,b) in constant time. +func ConstantTimeMin(a, b uint32) uint32 { + return b ^ ((a ^ b) & -ConstantTimeLess(a, b)) +} + +// ConstantTimeSelect64 returns a when cond == 1 or b when cond == 0 in constant +// time. +// +// WARNING: The behavior is undefined if cond is anything other than 0 or 1. +func ConstantTimeSelect64(cond, a, b uint64) uint64 { + mask := -cond + return b ^ (a^b)&mask +} diff --git a/dcrec/secp256k1/common_test.go b/dcrec/secp256k1/internal/arith/consttime_test.go similarity index 93% rename from dcrec/secp256k1/common_test.go rename to dcrec/secp256k1/internal/arith/consttime_test.go index cde06019ff..08a817ea8c 100644 --- a/dcrec/secp256k1/common_test.go +++ b/dcrec/secp256k1/internal/arith/consttime_test.go @@ -3,7 +3,7 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package secp256k1 +package arith import "testing" @@ -28,7 +28,7 @@ func TestConstantTimeSelect64(t *testing.T) { } for _, test := range tests { - got := constantTimeSelect64(test.cond, test.a, test.b) + got := ConstantTimeSelect64(test.cond, test.a, test.b) if got != test.want { t.Errorf("%q: unexpected result -- got %d, want %d", test.name, got, test.want) diff --git a/dcrec/secp256k1/modnscalar.go b/dcrec/secp256k1/modnscalar.go index f3e2df4eab..981b7174a6 100644 --- a/dcrec/secp256k1/modnscalar.go +++ b/dcrec/secp256k1/modnscalar.go @@ -10,6 +10,8 @@ import ( "math/big" "math/bits" "sync" + + "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/arith" ) // References: @@ -169,7 +171,7 @@ func (s *ModNScalar) Zero() { // operations require a numeric value. See [ModNScalar.IsZero] for the version // that returns a bool. func (s *ModNScalar) IsZeroBit() uint32 { - return constantTimeEq64(s.n[0]|s.n[1]|s.n[2]|s.n[3], 0) + return arith.ConstantTimeEq64(s.n[0]|s.n[1]|s.n[2]|s.n[3], 0) } // IsZero returns whether or not the scalar is equal to zero in constant time. @@ -228,10 +230,10 @@ func (s *ModNScalar) SetBytes(b *[32]byte) uint32 { // Constant-time select. // // Set s = s when s < N (aka borrow is set). Otherwise s = t = s - N. - s.n[0] = constantTimeSelect64(borrow, s.n[0], t0) - s.n[1] = constantTimeSelect64(borrow, s.n[1], t1) - s.n[2] = constantTimeSelect64(borrow, s.n[2], t2) - s.n[3] = constantTimeSelect64(borrow, s.n[3], t3) + s.n[0] = arith.ConstantTimeSelect64(borrow, s.n[0], t0) + s.n[1] = arith.ConstantTimeSelect64(borrow, s.n[1], t1) + s.n[2] = arith.ConstantTimeSelect64(borrow, s.n[2], t2) + s.n[3] = arith.ConstantTimeSelect64(borrow, s.n[3], t3) return uint32(1 - borrow) } @@ -255,7 +257,7 @@ func (s *ModNScalar) SetByteSlice(b []byte) bool { // Always copy a total of 32 bytes regardless of the input length to avoid // introducing data-dependent timing. var b32 [32]byte - b = b[:constantTimeMin(uint32(len(b)), 32)] + b = b[:arith.ConstantTimeMin(uint32(len(b)), 32)] copy(b32[:], b32[:32-len(b)]) copy(b32[32-len(b):], b) result := s.SetBytes(&b32) @@ -362,10 +364,10 @@ func (s *ModNScalar) Add2(a, b *ModNScalar) *ModNScalar { // Set s = t = a+b only when there was no overflow and t < N (borrow set). // Otherwise s = u = a+b - N. cond := (1 - overflow) & borrow - s.n[0] = constantTimeSelect64(cond, t0, u0) - s.n[1] = constantTimeSelect64(cond, t1, u1) - s.n[2] = constantTimeSelect64(cond, t2, u2) - s.n[3] = constantTimeSelect64(cond, t3, u3) + s.n[0] = arith.ConstantTimeSelect64(cond, t0, u0) + s.n[1] = arith.ConstantTimeSelect64(cond, t1, u1) + s.n[2] = arith.ConstantTimeSelect64(cond, t2, u2) + s.n[3] = arith.ConstantTimeSelect64(cond, t3, u3) return s } @@ -731,10 +733,10 @@ func scalar64Reduce512(r *[4]uint64, x *[8]uint64) { // // Set r = t only when t < N (borrow set). // Otherwise r = s = t - N. - r[0] = constantTimeSelect64(borrow, t0, s0) - r[1] = constantTimeSelect64(borrow, t1, s1) - r[2] = constantTimeSelect64(borrow, t2, s2) - r[3] = constantTimeSelect64(borrow, t3, s3) + r[0] = arith.ConstantTimeSelect64(borrow, t0, s0) + r[1] = arith.ConstantTimeSelect64(borrow, t1, s1) + r[2] = arith.ConstantTimeSelect64(borrow, t2, s2) + r[3] = arith.ConstantTimeSelect64(borrow, t3, s3) } // Mul2 multiplies the passed two scalars together modulo the group order in @@ -799,7 +801,8 @@ func (s *ModNScalar) NegateVal(val *ModNScalar) *ModNScalar { // being about 3-4% slower on 64-bit hardware. // // Determine mask first to allow aliasing. - mask := -uint64(constantTimeNotEq64(val.n[0]|val.n[1]|val.n[2]|val.n[3], 0)) + combinedLimbs := val.n[0] | val.n[1] | val.n[2] | val.n[3] + mask := -uint64(arith.ConstantTimeNotEq64(combinedLimbs, 0)) // Unconditionally subtract the scalar from the group order. // From ebed98da03e1ce17dae91125e3b035f94a04c3b1 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sat, 25 Jul 2026 06:04:43 -0500 Subject: [PATCH 13/33] secp256k1: Add constant time helper tests. This adds dedicated tests for the constant time helpers in the new internal arith package. While the helpers are already well exercised by the upper layers that use them, testing the primitives directly helps ensure their correctness in isolation. --- .../internal/arith/consttime_test.go | 280 +++++++++++++++++- 1 file changed, 267 insertions(+), 13 deletions(-) diff --git a/dcrec/secp256k1/internal/arith/consttime_test.go b/dcrec/secp256k1/internal/arith/consttime_test.go index 08a817ea8c..35941ee8bc 100644 --- a/dcrec/secp256k1/internal/arith/consttime_test.go +++ b/dcrec/secp256k1/internal/arith/consttime_test.go @@ -7,31 +7,285 @@ package arith import "testing" -// TestConstantTimeSelect64 ensures that the 64-bit constant time selection -// function works as expected in terms of behavior. +// TestConstantTimeEq ensures [ConstantTimeEq] returns the expected results. +func TestConstantTimeEq(t *testing.T) { + tests := []struct { + a uint32 // first value + b uint32 // second value + want uint32 // expected result (0 or 1) + }{ + {0, 0, 1}, + {1, 1, 1}, + {1<<32 - 1, 1<<32 - 1, 1}, + {0x12345678, 0x12345678, 1}, + {0, 1, 0}, + {1, 0, 0}, + {1<<32 - 1, 0, 0}, + {0x12345678, 0x87654321, 0}, + {0, 1 << 31, 0}, + {1 << 31, 1 << 31, 1}, + {1, 2, 0}, + {1<<32 - 2, 1<<32 - 1, 0}, + } + + for _, test := range tests { + got := ConstantTimeEq(test.a, test.b) + if got != test.want { + t.Errorf("%08x == %08x: got %d, want %d", test.a, test.b, got, + test.want) + } + } +} + +// TestConstantTimeEq64 ensures [ConstantTimeEq64] returns the expected results. +func TestConstantTimeEq64(t *testing.T) { + tests := []struct { + a uint64 // first value + b uint64 // second value + want uint32 // expected result (0 or 1) + }{ + {0, 0, 1}, + {1, 1, 1}, + {1<<64 - 1, 1<<64 - 1, 1}, + {0x123456789abcdef0, 0x123456789abcdef0, 1}, + {0, 1, 0}, + {1, 0, 0}, + {1<<64 - 1, 0, 0}, + {0x123456789abcdef0, 0xfedcba9876543210, 0}, + {0, 1 << 63, 0}, + {1 << 63, 1 << 63, 1}, + {1, 2, 0}, + {1<<64 - 2, 1<<64 - 1, 0}, + } + + for _, test := range tests { + got := ConstantTimeEq64(test.a, test.b) + if got != test.want { + t.Errorf("%016x == %016x: got %d, want %d", test.a, test.b, got, + test.want) + } + } +} + +// TestConstantTimeNotEq64 ensures [ConstantTimeNotEq64] returns the expected +// results. +func TestConstantTimeNotEq64(t *testing.T) { + tests := []struct { + a uint64 // first value + b uint64 // second value + want uint32 // expected result + }{ + {0, 0, 0}, + {1, 1, 0}, + {1<<64 - 1, 1<<64 - 1, 0}, + {0x123456789abcdef0, 0x123456789abcdef0, 0}, + {0, 1, 1}, + {1, 0, 1}, + {1<<64 - 1, 0, 1}, + {0x123456789abcdef0, 0xfedcba9876543210, 1}, + {0, 1 << 63, 1}, + {1 << 63, 1 << 63, 0}, + {1, 2, 1}, + {1<<64 - 2, 1<<64 - 1, 1}, + } + + for _, test := range tests { + got := ConstantTimeNotEq64(test.a, test.b) + if got != test.want { + t.Errorf("%016x != %016x: got %d, want %d", test.a, test.b, got, + test.want) + } + } +} + +// TestConstantTimeLess ensures [ConstantTimeLess] returns the expected results. +func TestConstantTimeLess(t *testing.T) { + tests := []struct { + a uint32 // first value + b uint32 // second value + want uint32 // expected result (0 or 1) + }{ + {0, 1, 1}, + {1, 2, 1}, + {0, 1<<32 - 1, 1}, + {1<<31 - 1, 1 << 31, 1}, + {1<<32 - 2, 1<<32 - 1, 1}, + {0x12345678, 0x87654321, 1}, + {1, 0, 0}, + {1, 1, 0}, + {1<<32 - 1, 0, 0}, + {1<<32 - 1, 1<<32 - 1, 0}, + {0x87654321, 0x12345678, 0}, + {0, 0, 0}, + {1 << 31, 1<<32 - 1, 1}, + } + + for _, test := range tests { + got := ConstantTimeLess(test.a, test.b) + if got != test.want { + t.Errorf("%08x < %08x: got %d, want %d", test.a, test.b, got, + test.want) + } + } +} + +// TestConstantTimeLessOrEq ensures [ConstantTimeLessOrEq] returns the expected +// results. +func TestConstantTimeLessOrEq(t *testing.T) { + tests := []struct { + a uint32 // first value + b uint32 // second value + want uint32 // expected result (0 or 1) + }{ + {0, 0, 1}, + {0, 1, 1}, + {1, 2, 1}, + {1, 1, 1}, + {0, 1<<32 - 1, 1}, + {0x12345678, 0x87654321, 1}, + {1<<32 - 1, 1<<32 - 1, 1}, + {1<<32 - 2, 1<<32 - 1, 1}, + {1, 0, 0}, + {2, 1, 0}, + {1<<32 - 1, 0, 0}, + {0x87654321, 0x12345678, 0}, + {1 << 31, 1<<32 - 1, 1}, + {1 << 31, 1 << 31, 1}, + } + + for _, test := range tests { + got := ConstantTimeLessOrEq(test.a, test.b) + if got != test.want { + t.Errorf("%08x <= %08x: got %d, want %d", test.a, test.b, got, + test.want) + } + } +} + +// TestConstantTimeGreater ensures [ConstantTimeGreater] returns the expected +// results. +func TestConstantTimeGreater(t *testing.T) { + tests := []struct { + a uint32 // first value + b uint32 // second value + want uint32 // expected result (0 or 1) + }{ + {1, 0, 1}, + {2, 1, 1}, + {1<<32 - 1, 0, 1}, + {1<<32 - 1, 1<<32 - 2, 1}, + {0x87654321, 0x12345678, 1}, + {0, 1, 0}, + {1, 1, 0}, + {0, 1<<32 - 1, 0}, + {1<<32 - 1, 1<<32 - 1, 0}, + {0x12345678, 0x87654321, 0}, + {0, 0, 0}, + {1<<32 - 1, 1 << 31, 1}, + } + + for _, test := range tests { + got := ConstantTimeGreater(test.a, test.b) + if got != test.want { + t.Errorf("%08x > %08x: got %d, want %d", test.a, test.b, got, + test.want) + } + } +} + +// TestConstantTimeGreaterOrEq ensures [ConstantTimeGreaterOrEq] returns the +// expected results. +func TestConstantTimeGreaterOrEq(t *testing.T) { + tests := []struct { + a uint32 // first value + b uint32 // second value + want uint32 // expected result (0 or 1) + }{ + {0, 0, 1}, + {1, 0, 1}, + {1, 1, 1}, + {2, 1, 1}, + {1<<32 - 1, 0, 1}, + {1<<32 - 1, 1<<32 - 1, 1}, + {1<<32 - 1, 1<<32 - 2, 1}, + {0x87654321, 0x12345678, 1}, + {0, 1, 0}, + {1, 2, 0}, + {0, 1<<32 - 1, 0}, + {0x12345678, 0x87654321, 0}, + {1 << 31, 1 << 31, 1}, + {1 << 31, 1<<32 - 1, 0}, + } + + for _, test := range tests { + got := ConstantTimeGreaterOrEq(test.a, test.b) + if got != test.want { + t.Errorf("%08x >= %08x: got %d, want %d", test.a, test.b, got, + test.want) + } + } +} + +// TestConstantTimeMin ensures [ConstantTimeMin] returns the expected results. +func TestConstantTimeMin(t *testing.T) { + tests := []struct { + a uint32 // first value + b uint32 // second value + want uint32 // expected minimum value + }{ + {0, 1, 0}, + {1, 2, 1}, + {0, 1<<32 - 1, 0}, + {1<<32 - 2, 1<<32 - 1, 1<<32 - 2}, + {0x12345678, 0x87654321, 0x12345678}, + {1, 0, 0}, + {2, 1, 1}, + {1<<32 - 1, 0, 0}, + {1<<32 - 1, 1<<32 - 2, 1<<32 - 2}, + {0x87654321, 0x12345678, 0x12345678}, + {0, 0, 0}, + {1, 1, 1}, + {1<<32 - 1, 1<<32 - 1, 1<<32 - 1}, + {0x12345678, 0x12345678, 0x12345678}, + {1 << 31, 1<<32 - 1, 1 << 31}, + {1<<32 - 1, 1 << 31, 1 << 31}, + } + + for _, test := range tests { + got := ConstantTimeMin(test.a, test.b) + if got != test.want { + t.Errorf("min(%08x, %08x): got %08x, want %08x", test.a, test.b, + got, test.want) + } + } +} + +// TestConstantTimeSelect64 ensures [ConstantTimeSelect64] returns the expected +// results. func TestConstantTimeSelect64(t *testing.T) { tests := []struct { - name string // test description cond uint64 // condition value a uint64 // first value b uint64 // second value want uint64 // expected selected value }{ - {"sel(0,1,2)==2", 0, 1, 2, 2}, - {"sel(1,1,2)==1", 1, 1, 2, 1}, - {"sel(0,1<<64-1,1)==1", 0, 1<<64 - 1, 1, 1}, - {"sel(1,1<<64-1,1)==1<<64-1", 1, 1<<64 - 1, 1, 1<<64 - 1}, - {"sel(0,1,1<<64-1)==1<<64-1", 0, 1, 1<<64 - 1, 1<<64 - 1}, - {"sel(1,1,1<<64-1)==1", 1, 1, 1<<64 - 1, 1}, - {"sel(0,1<<64-1,1<<64-2)==1<<64-2", 0, 1<<64 - 1, 1<<64 - 2, 1<<64 - 2}, - {"sel(1,1<<64-1,1<<64-2)==1<<64-1", 1, 1<<64 - 1, 1<<64 - 2, 1<<64 - 1}, + {0, 1, 2, 2}, + {1, 1, 2, 1}, + {0, 1<<64 - 1, 1, 1}, + {1, 1<<64 - 1, 1, 1<<64 - 1}, + {0, 1, 1<<64 - 1, 1<<64 - 1}, + {1, 1, 1<<64 - 1, 1}, + {0, 1<<64 - 1, 1<<64 - 2, 1<<64 - 2}, + {1, 1<<64 - 1, 1<<64 - 2, 1<<64 - 1}, + {0, 0x123456789abcdef0, 0x123456789abcdef0, 0x123456789abcdef0}, + {1, 0x123456789abcdef0, 0x123456789abcdef0, 0x123456789abcdef0}, } for _, test := range tests { got := ConstantTimeSelect64(test.cond, test.a, test.b) if got != test.want { - t.Errorf("%q: unexpected result -- got %d, want %d", test.name, - got, test.want) + t.Errorf("sel(%d, %016x, %016x): got %016x, want %016x", test.cond, + test.a, test.b, got, test.want) } } } From 9f2a7e3ee757a5f439de889d1b4f57800eb448dd Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sat, 25 Jul 2026 04:26:20 -0500 Subject: [PATCH 14/33] secp256k1: Move mul/square primitives to arith. The package currently defines modulus-agnostic 512-bit multiplication and squaring primitives as part of the 64-bit field implementation despite them also being used by the scalar implementation. They are also shared by the field implementations that will be split into separate packages in future commits to prepare for architecture-specific backend selection. In order to smooth that transition, this moves the primitives into the internal arith package and updates all call sites to use them. No functional changes are intended. The move is purely organizational and preserves the existing arithmetic. --- dcrec/secp256k1/field64.go | 203 ----------------------- dcrec/secp256k1/field64_amd64.go | 6 +- dcrec/secp256k1/field64_generic.go | 6 +- dcrec/secp256k1/internal/arith/arith.go | 209 ++++++++++++++++++++++++ dcrec/secp256k1/modnscalar.go | 4 +- 5 files changed, 219 insertions(+), 209 deletions(-) create mode 100644 dcrec/secp256k1/internal/arith/arith.go diff --git a/dcrec/secp256k1/field64.go b/dcrec/secp256k1/field64.go index 30afab1e2b..3968ca6ea6 100644 --- a/dcrec/secp256k1/field64.go +++ b/dcrec/secp256k1/field64.go @@ -653,209 +653,6 @@ func (f *FieldVal64) SquareVal(val *FieldVal64) *FieldVal64 { return f } -// field64Mul512 sets t = x * y as an unreduced 512-bit product via a row-by-row -// schoolbook multiply. -func field64Mul512(t *[8]uint64, x, y *[4]uint64) { - // The intermediate bounds and carry assumptions used by this algorithm have - // been formally verified. The verification artifacts are available in - // internal/proofs. - - a0, a1, a2, a3 := x[0], x[1], x[2], x[3] - b0, b1, b2, b3 := y[0], y[1], y[2], y[3] - - var c uint64 - - // Row 0: p0..p4 = a * b0. - // - // Note that since h3 is the upper 64 bits of the product of two uint64s: - // h3 ≤ floor((2^64-1)^2 / 2^64) = 2^64 - 2 - // - // Without any other considerations, c ≤ 1, so a loose bound is: - // p4 ≤ h3 + 1 = 2^64 - 1 < 2^64 - // - // This already shows that the carryless add in p4 is safe, however, a tight - // upper bound is more useful to prove no overflow is possible in the upper - // words of the subsequent rows. - // - // Claim: p4 ≤ 2^64 - 2 - // - // Consider the row product A*b, where A ≤ 2^256 - 1, b ≤ 2^64 - 1, then: - // A*b ≤ (2^256 - 1)(2^64 - 1) = 2^320 - 2^256 - 2^64 + 1 - // - // Next, expressing the product in base 2^256 gives: - // A*b = p4*2^256 + qlow - // - // Where qlow is the low 256 bits of the product and p4 is the integer - // quotient: - // p4 = floor(A*b / 2^256) - // qlow = A*b (mod 2^256) - // - // Finally, bound the quotient: - // p4 = floor(A*b / 2^256) - // ≤ floor((2^320 - 2^256 - 2^64 + 1) / 2^256) - // = floor(2^64 - 1 - 2^(-192) + 2^(-256)) - // ≤ 2^64 - 2 - // - // So, p4 ≤ 2^64 - 2. - h0, p0 := bits.Mul64(a0, b0) - h1, p1 := bits.Mul64(a1, b0) - h2, p2 := bits.Mul64(a2, b0) - h3, p3 := bits.Mul64(a3, b0) - p1, c = bits.Add64(p1, h0, 0) - p2, c = bits.Add64(p2, h1, c) - p3, c = bits.Add64(p3, h2, c) - p4 := h3 + c - - // Row 1: p1..p5 += a * b1. - // - // Per row 0 above, the tight bound on q4 for this row is: - // q4 ≤ 2^64 - 2 - // - // Since c ≤ 1: - // p5 ≤ q4 + 1 = 2^64 - 1 < 2^64 - // - // So, the carryless add in p5 is safe. - h0, q0 := bits.Mul64(a0, b1) - h1, q1 := bits.Mul64(a1, b1) - h2, q2 := bits.Mul64(a2, b1) - h3, q3 := bits.Mul64(a3, b1) - q1, c = bits.Add64(q1, h0, 0) - q2, c = bits.Add64(q2, h1, c) - q3, c = bits.Add64(q3, h2, c) - q4 := h3 + c - p1, c = bits.Add64(p1, q0, 0) - p2, c = bits.Add64(p2, q1, c) - p3, c = bits.Add64(p3, q2, c) - p4, c = bits.Add64(p4, q3, c) - p5 := q4 + c - - // Row 2: p2..p6 += a * b2. - // - // The same bounds calculation as row 1 applies. - h0, q0 = bits.Mul64(a0, b2) - h1, q1 = bits.Mul64(a1, b2) - h2, q2 = bits.Mul64(a2, b2) - h3, q3 = bits.Mul64(a3, b2) - q1, c = bits.Add64(q1, h0, 0) - q2, c = bits.Add64(q2, h1, c) - q3, c = bits.Add64(q3, h2, c) - q4 = h3 + c - p2, c = bits.Add64(p2, q0, 0) - p3, c = bits.Add64(p3, q1, c) - p4, c = bits.Add64(p4, q2, c) - p5, c = bits.Add64(p5, q3, c) - p6 := q4 + c - - // Row 3: p3..p7 += a * b3. - // - // The same bounds calculation as row 1 applies. - h0, q0 = bits.Mul64(a0, b3) - h1, q1 = bits.Mul64(a1, b3) - h2, q2 = bits.Mul64(a2, b3) - h3, q3 = bits.Mul64(a3, b3) - q1, c = bits.Add64(q1, h0, 0) - q2, c = bits.Add64(q2, h1, c) - q3, c = bits.Add64(q3, h2, c) - q4 = h3 + c - p3, c = bits.Add64(p3, q0, 0) - p4, c = bits.Add64(p4, q1, c) - p5, c = bits.Add64(p5, q2, c) - p6, c = bits.Add64(p6, q3, c) - p7 := q4 + c - - t[0], t[1], t[2], t[3] = p0, p1, p2, p3 - t[4], t[5], t[6], t[7] = p4, p5, p6, p7 -} - -// field64Square512 sets t = a^2 as an unreduced 512-bit product. -func field64Square512(t *[8]uint64, a *[4]uint64) { - // The intermediate bounds and carry assumptions used by this algorithm have - // been formally verified. The verification artifacts are available in - // internal/proofs. - - a0, a1, a2, a3 := a[0], a[1], a[2], a[3] - - var c uint64 - - // Off-diagonal upper-triangle products (not yet doubled). - // - // Note that since h03 is the upper 64 bits of the product of two uint64s: - // h03 ≤ floor((2^64-1)^2 / 2^64) = 2^64 - 2 - // - // Then, because c ≤ 1, a loose bound is: - // p4 ≤ h03 + 1 = 2^64 - 1 < 2^64 - // - // Therefore, it is safe to discard the carry. - p2, p1 := bits.Mul64(a0, a1) - h02, l02 := bits.Mul64(a0, a2) - h03, l03 := bits.Mul64(a0, a3) - p2, c = bits.Add64(p2, l02, 0) - p3, c := bits.Add64(h02, l03, c) - p4, _ := bits.Add64(h03, 0, c) - - h12, l12 := bits.Mul64(a1, a2) - p3, c = bits.Add64(p3, l12, 0) - p4, c = bits.Add64(p4, h12, c) - p5 := c - - // The p5 carry is safe to discard because p5 + h13 + c ≤ 2^64 - 1 (where c - // is the carry from p4 + l13). - // - // A full proof involves case analysis that is omitted here since the - // impossibility of the carry is formally proven in internal/proofs, but the - // key point is that the only way the final add could have a carry is if all - // 3 of the following conditions were simultaneously true: - // - // 1) p5_old = 1 (the carry from the earlier chain, so ≤ 1) - // 2) h13 = 2^64 - 2 (h13 ≤ 2^64 - 2 as proven previously) - // 3) c = 1 (implies p4 + l13 ≥ 2^64) - // - // However, that combination of conditions is impossible because in order - // for condition 2 to be true, a1 = a3 = 2^64 - 1, in which case l13 = 1 - // and so in order for condition 3 to also be true, p4 = 2^64 - 1. But then - // the combination of those conditions forces p5_old = 0. - h13, l13 := bits.Mul64(a1, a3) - p4, c = bits.Add64(p4, l13, 0) - p5, _ = bits.Add64(p5, h13, c) - - // Similarly, the p6 carry is safe to discard because, per above: - // h23 ≤ 2^64 - 2 - // - // Then, again c ≤ 1, so the same loose bound applies: - // p6 ≤ h23 + 1 = 2^64 - 1 < 2^64 - h23, l23 := bits.Mul64(a2, a3) - p5, c = bits.Add64(p5, l23, 0) - p6, _ := bits.Add64(h23, 0, c) - - // Double p1..p6, capturing the top carry into p7. - p1, c = bits.Add64(p1, p1, 0) - p2, c = bits.Add64(p2, p2, c) - p3, c = bits.Add64(p3, p3, c) - p4, c = bits.Add64(p4, p4, c) - p5, c = bits.Add64(p5, p5, c) - p6, c = bits.Add64(p6, p6, c) - p7 := c - - // Add the diagonal squares a[i]^2 at columns 0,2,4,6 in one carry chain. - // - // The carry on the final add is safe to discard because a < p < 2^256, so: - // (2^256 - 1)^2 = 2^512 - 2^257 + 1 < 2^512 - h0, p0 := bits.Mul64(a0, a0) - h1, l1 := bits.Mul64(a1, a1) - h2, l2 := bits.Mul64(a2, a2) - h3, l3 := bits.Mul64(a3, a3) - p1, c = bits.Add64(p1, h0, 0) - p2, c = bits.Add64(p2, l1, c) - p3, c = bits.Add64(p3, h1, c) - p4, c = bits.Add64(p4, l2, c) - p5, c = bits.Add64(p5, h2, c) - p6, c = bits.Add64(p6, l3, c) - p7, _ = bits.Add64(p7, h3, c) - - t[0], t[1], t[2], t[3] = p0, p1, p2, p3 - t[4], t[5], t[6], t[7] = p4, p5, p6, p7 -} - // field64Reduce512 reduces a 512-bit little-endian limb array modulo p in // constant time and stores the result in r. func field64Reduce512(r *[4]uint64, x *[8]uint64) { diff --git a/dcrec/secp256k1/field64_amd64.go b/dcrec/secp256k1/field64_amd64.go index bf068120f4..bef8f12cc2 100644 --- a/dcrec/secp256k1/field64_amd64.go +++ b/dcrec/secp256k1/field64_amd64.go @@ -6,6 +6,8 @@ package secp256k1 +import "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/arith" + // field64UseADX reports whether the CPU supports both BMI2 (MULX) and ADX (ADCX/ADOX). var field64UseADX = func() bool { const ( @@ -59,13 +61,13 @@ func field64Square(r *[4]uint64, a *[4]uint64) { // field64MulGeneric sets r = a * b (mod p) func field64MulGeneric(r *[4]uint64, a, b *[4]uint64) { var product [8]uint64 - field64Mul512(&product, a, b) + arith.Mul512(&product, a, b) field64Reduce512(r, &product) } // field64SquareGeneric sets r = a^2 (mod p) func field64SquareGeneric(r *[4]uint64, a *[4]uint64) { var product [8]uint64 - field64Square512(&product, a) + arith.Square512(&product, a) field64Reduce512(r, &product) } diff --git a/dcrec/secp256k1/field64_generic.go b/dcrec/secp256k1/field64_generic.go index bea665bc69..6ad4be30a7 100644 --- a/dcrec/secp256k1/field64_generic.go +++ b/dcrec/secp256k1/field64_generic.go @@ -6,16 +6,18 @@ package secp256k1 +import "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/arith" + // field64Mul sets r = a * b (mod p). func field64Mul(r *[4]uint64, a, b *[4]uint64) { var product [8]uint64 - field64Mul512(&product, a, b) + arith.Mul512(&product, a, b) field64Reduce512(r, &product) } // field64Square sets r = a^2 (mod p). func field64Square(r *[4]uint64, a *[4]uint64) { var product [8]uint64 - field64Square512(&product, a) + arith.Square512(&product, a) field64Reduce512(r, &product) } diff --git a/dcrec/secp256k1/internal/arith/arith.go b/dcrec/secp256k1/internal/arith/arith.go new file mode 100644 index 0000000000..13b35d6737 --- /dev/null +++ b/dcrec/secp256k1/internal/arith/arith.go @@ -0,0 +1,209 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package arith + +import "math/bits" + +// Mul512 sets t = x * y as an unreduced 512-bit product. +func Mul512(t *[8]uint64, x, y *[4]uint64) { + // The intermediate bounds and carry assumptions used by this algorithm have + // been formally verified. The verification artifacts are available in + // internal/proofs. + + a0, a1, a2, a3 := x[0], x[1], x[2], x[3] + b0, b1, b2, b3 := y[0], y[1], y[2], y[3] + + var c uint64 + + // Row 0: p0..p4 = a * b0. + // + // Note that since h3 is the upper 64 bits of the product of two uint64s: + // h3 ≤ floor((2^64-1)^2 / 2^64) = 2^64 - 2 + // + // Without any other considerations, c ≤ 1, so a loose bound is: + // p4 ≤ h3 + 1 = 2^64 - 1 < 2^64 + // + // This already shows that the carryless add in p4 is safe, however, a tight + // upper bound is more useful to prove no overflow is possible in the upper + // words of the subsequent rows. + // + // Claim: p4 ≤ 2^64 - 2 + // + // Consider the row product A*b, where A ≤ 2^256 - 1, b ≤ 2^64 - 1, then: + // A*b ≤ (2^256 - 1)(2^64 - 1) = 2^320 - 2^256 - 2^64 + 1 + // + // Next, expressing the product in base 2^256 gives: + // A*b = p4*2^256 + qlow + // + // Where qlow is the low 256 bits of the product and p4 is the integer + // quotient: + // p4 = floor(A*b / 2^256) + // qlow = A*b (mod 2^256) + // + // Finally, bound the quotient: + // p4 = floor(A*b / 2^256) + // ≤ floor((2^320 - 2^256 - 2^64 + 1) / 2^256) + // = floor(2^64 - 1 - 2^(-192) + 2^(-256)) + // ≤ 2^64 - 2 + // + // So, p4 ≤ 2^64 - 2. + h0, p0 := bits.Mul64(a0, b0) + h1, p1 := bits.Mul64(a1, b0) + h2, p2 := bits.Mul64(a2, b0) + h3, p3 := bits.Mul64(a3, b0) + p1, c = bits.Add64(p1, h0, 0) + p2, c = bits.Add64(p2, h1, c) + p3, c = bits.Add64(p3, h2, c) + p4 := h3 + c + + // Row 1: p1..p5 += a * b1. + // + // Per row 0 above, the tight bound on q4 for this row is: + // q4 ≤ 2^64 - 2 + // + // Since c ≤ 1: + // p5 ≤ q4 + 1 = 2^64 - 1 < 2^64 + // + // So, the carryless add in p5 is safe. + h0, q0 := bits.Mul64(a0, b1) + h1, q1 := bits.Mul64(a1, b1) + h2, q2 := bits.Mul64(a2, b1) + h3, q3 := bits.Mul64(a3, b1) + q1, c = bits.Add64(q1, h0, 0) + q2, c = bits.Add64(q2, h1, c) + q3, c = bits.Add64(q3, h2, c) + q4 := h3 + c + p1, c = bits.Add64(p1, q0, 0) + p2, c = bits.Add64(p2, q1, c) + p3, c = bits.Add64(p3, q2, c) + p4, c = bits.Add64(p4, q3, c) + p5 := q4 + c + + // Row 2: p2..p6 += a * b2. + // + // The same bounds calculation as row 1 applies. + h0, q0 = bits.Mul64(a0, b2) + h1, q1 = bits.Mul64(a1, b2) + h2, q2 = bits.Mul64(a2, b2) + h3, q3 = bits.Mul64(a3, b2) + q1, c = bits.Add64(q1, h0, 0) + q2, c = bits.Add64(q2, h1, c) + q3, c = bits.Add64(q3, h2, c) + q4 = h3 + c + p2, c = bits.Add64(p2, q0, 0) + p3, c = bits.Add64(p3, q1, c) + p4, c = bits.Add64(p4, q2, c) + p5, c = bits.Add64(p5, q3, c) + p6 := q4 + c + + // Row 3: p3..p7 += a * b3. + // + // The same bounds calculation as row 1 applies. + h0, q0 = bits.Mul64(a0, b3) + h1, q1 = bits.Mul64(a1, b3) + h2, q2 = bits.Mul64(a2, b3) + h3, q3 = bits.Mul64(a3, b3) + q1, c = bits.Add64(q1, h0, 0) + q2, c = bits.Add64(q2, h1, c) + q3, c = bits.Add64(q3, h2, c) + q4 = h3 + c + p3, c = bits.Add64(p3, q0, 0) + p4, c = bits.Add64(p4, q1, c) + p5, c = bits.Add64(p5, q2, c) + p6, c = bits.Add64(p6, q3, c) + p7 := q4 + c + + t[0], t[1], t[2], t[3] = p0, p1, p2, p3 + t[4], t[5], t[6], t[7] = p4, p5, p6, p7 +} + +// Square512 sets t = a^2 as an unreduced 512-bit product. +func Square512(t *[8]uint64, a *[4]uint64) { + // The intermediate bounds and carry assumptions used by this algorithm have + // been formally verified. The verification artifacts are available in + // internal/proofs. + + a0, a1, a2, a3 := a[0], a[1], a[2], a[3] + + var c uint64 + + // Off-diagonal upper-triangle products (not yet doubled). + // + // Note that since h03 is the upper 64 bits of the product of two uint64s: + // h03 ≤ floor((2^64-1)^2 / 2^64) = 2^64 - 2 + // + // Then, because c ≤ 1, a loose bound is: + // p4 ≤ h03 + 1 = 2^64 - 1 < 2^64 + // + // Therefore, it is safe to discard the carry. + p2, p1 := bits.Mul64(a0, a1) + h02, l02 := bits.Mul64(a0, a2) + h03, l03 := bits.Mul64(a0, a3) + p2, c = bits.Add64(p2, l02, 0) + p3, c := bits.Add64(h02, l03, c) + p4, _ := bits.Add64(h03, 0, c) + + h12, l12 := bits.Mul64(a1, a2) + p3, c = bits.Add64(p3, l12, 0) + p4, c = bits.Add64(p4, h12, c) + p5 := c + + // The p5 carry is safe to discard because p5 + h13 + c ≤ 2^64 - 1 (where c + // is the carry from p4 + l13). + // + // A full proof involves case analysis that is omitted here since the + // impossibility of the carry is formally proven in internal/proofs, but the + // key point is that the only way the final add could have a carry is if all + // 3 of the following conditions were simultaneously true: + // + // 1) p5_old = 1 (the carry from the earlier chain, so ≤ 1) + // 2) h13 = 2^64 - 2 (h13 ≤ 2^64 - 2 as proven previously) + // 3) c = 1 (implies p4 + l13 ≥ 2^64) + // + // However, that combination of conditions is impossible because in order + // for condition 2 to be true, a1 = a3 = 2^64 - 1, in which case l13 = 1 + // and so in order for condition 3 to also be true, p4 = 2^64 - 1. But then + // the combination of those conditions forces p5_old = 0. + h13, l13 := bits.Mul64(a1, a3) + p4, c = bits.Add64(p4, l13, 0) + p5, _ = bits.Add64(p5, h13, c) + + // Similarly, the p6 carry is safe to discard because, per above: + // h23 ≤ 2^64 - 2 + // + // Then, again c ≤ 1, so the same loose bound applies: + // p6 ≤ h23 + 1 = 2^64 - 1 < 2^64 + h23, l23 := bits.Mul64(a2, a3) + p5, c = bits.Add64(p5, l23, 0) + p6, _ := bits.Add64(h23, 0, c) + + // Double p1..p6, capturing the top carry into p7. + p1, c = bits.Add64(p1, p1, 0) + p2, c = bits.Add64(p2, p2, c) + p3, c = bits.Add64(p3, p3, c) + p4, c = bits.Add64(p4, p4, c) + p5, c = bits.Add64(p5, p5, c) + p6, c = bits.Add64(p6, p6, c) + p7 := c + + // Add the diagonal squares a[i]^2 at columns 0,2,4,6 in one carry chain. + // + // The carry on the final add is safe to discard because a < p < 2^256, so: + // (2^256 - 1)^2 = 2^512 - 2^257 + 1 < 2^512 + h0, p0 := bits.Mul64(a0, a0) + h1, l1 := bits.Mul64(a1, a1) + h2, l2 := bits.Mul64(a2, a2) + h3, l3 := bits.Mul64(a3, a3) + p1, c = bits.Add64(p1, h0, 0) + p2, c = bits.Add64(p2, l1, c) + p3, c = bits.Add64(p3, h1, c) + p4, c = bits.Add64(p4, l2, c) + p5, c = bits.Add64(p5, h2, c) + p6, c = bits.Add64(p6, l3, c) + p7, _ = bits.Add64(p7, h3, c) + + t[0], t[1], t[2], t[3] = p0, p1, p2, p3 + t[4], t[5], t[6], t[7] = p4, p5, p6, p7 +} diff --git a/dcrec/secp256k1/modnscalar.go b/dcrec/secp256k1/modnscalar.go index 981b7174a6..1cb4d3e32a 100644 --- a/dcrec/secp256k1/modnscalar.go +++ b/dcrec/secp256k1/modnscalar.go @@ -746,7 +746,7 @@ func scalar64Reduce512(r *[4]uint64, x *[8]uint64) { // s3.Mul2(s, s2).AddInt(1) so that s3 = (s * s2) + 1. func (s *ModNScalar) Mul2(a, b *ModNScalar) *ModNScalar { var product [8]uint64 - field64Mul512(&product, &a.n, &b.n) + arith.Mul512(&product, &a.n, &b.n) scalar64Reduce512(&s.n, &product) return s } @@ -767,7 +767,7 @@ func (s *ModNScalar) Mul(val *ModNScalar) *ModNScalar { // s3.SquareVal(s).Mul(s) so that s3 = s^2 * s = s^3. func (s *ModNScalar) SquareVal(val *ModNScalar) *ModNScalar { var product [8]uint64 - field64Square512(&product, &val.n) + arith.Square512(&product, &val.n) scalar64Reduce512(&s.n, &product) return s } From 7167a0f4554bd58e019d95ef0c5f92e91f541ddf Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sun, 26 Jul 2026 04:21:21 -0500 Subject: [PATCH 15/33] secp256k1: Add Mul512 tests. This adds dedicated tests for the 512-bit multiplication in the new internal arith package. While it is already already well exercised by the upper layers that use it, testing primitives directly helps ensure correctness in isolation. --- dcrec/secp256k1/internal/arith/arith_test.go | 244 +++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 dcrec/secp256k1/internal/arith/arith_test.go diff --git a/dcrec/secp256k1/internal/arith/arith_test.go b/dcrec/secp256k1/internal/arith/arith_test.go new file mode 100644 index 0000000000..c2bc42abce --- /dev/null +++ b/dcrec/secp256k1/internal/arith/arith_test.go @@ -0,0 +1,244 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package arith + +import ( + "encoding/binary" + "fmt" + "math/big" + mrand "math/rand" + "testing" + "time" +) + +// mustBig converts the passed hex string into a big integer and will panic if +// there is an error. This is only provided for the hard-coded constants so +// errors in the source code can be detected. It will only (and must only) be +// called with hard-coded values. +func mustBig(s string) *big.Int { + val, ok := new(big.Int).SetString(s, 16) + if !ok { + panic("failed to parse big integer from hex: " + s) + } + return val +} + +// mustBigToUint256 converts a [big.Int] to an array of 4 uint64s representing a +// 256-bit little-endian value. It will panic if the big integer is larger than +// the maximum 256-bit value. This is only provided for the hard-coded +// constants and randomly-generated values that are known to be in range, so +// errors in the source code can be detected. +func mustBigToUint256(v *big.Int) [4]uint64 { + if v.BitLen() > 256 { + panic(fmt.Sprintf("big integer %x is larger than max uint256", v)) + } + + var buf [32]byte + v.FillBytes(buf[:]) + + var result [4]uint64 + for i := 0; i < 4; i++ { + result[i] = binary.BigEndian.Uint64(buf[32-((i+1)*8):]) + } + return result +} + +// uint512ToBig converts an array of 8 uint64s representing a 512-bit +// little-endian value to a [big.Int]. +func uint512ToBig(v [8]uint64) *big.Int { + var buf [64]byte + for i := 0; i < 8; i++ { + binary.BigEndian.PutUint64(buf[i*8:], v[7-i]) + } + + return new(big.Int).SetBytes(buf[:]) +} + +// randBig returns a random 256-bit [big.Int] created from the passed rng. +func randBig(t *testing.T, rng *mrand.Rand) *big.Int { + t.Helper() + + var buf [32]byte + if _, err := rng.Read(buf[:]); err != nil { + t.Fatalf("failed to read random: %v", err) + } + + return new(big.Int).SetBytes(buf[:]) +} + +// TestMul512 ensures [Mul512] returns the expected result by comparing the +// product against the [big.Int] result. It also tests commutativity and full +// buffer replacement. +func TestMul512(t *testing.T) { + tests := []struct { + name string // test description + x, y string // hex encoded multiplicands + }{{ + name: "all zero", + x: "0", + y: "0", + }, { + name: "zero * identity", + x: "0", + y: "1", + }, { + name: "zero * max uint256", + x: "0", + y: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, { + name: "identity", + x: "1", + y: "1", + }, { + name: "identity * max uint256", + x: "1", + y: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, { + name: "small values", + x: "2", + y: "3", + }, { + name: "unbalanced small values", + x: "1", + y: "ffffffffffffffff", + }, { + name: "small * max uint256", + x: "2", + y: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, { + name: "max uint64 * max uint64", + x: "ffffffffffffffff", + y: "ffffffffffffffff", + }, { + name: "max uint64 * 2*max uint64", + x: "ffffffffffffffff", + y: "1fffffffffffffffe", + }, { + name: "2^1 * 2^1", + x: "2", + y: "2", + }, { + name: "2^8 * 2^8", + x: "100", + y: "100", + }, { + name: "2^16 * 2^16", + x: "10000", + y: "10000", + }, { + name: "2^32 * 2^32", + x: "100000000", + y: "100000000", + }, { + name: "2^64 * 2^64", + x: "10000000000000000", + y: "10000000000000000", + }, { + name: "2^128 * 2^128", + x: "100000000000000000000000000000000", + y: "100000000000000000000000000000000", + }, { + name: "2^192 * 2^64", + x: "1000000000000000000000000000000000000000000000000", + y: "10000000000000000", + }, { + name: "2^255 * 2^255", + x: "8000000000000000000000000000000000000000000000000000000000000000", + y: "8000000000000000000000000000000000000000000000000000000000000000", + }, { + name: "max uint256 minus one", + x: "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", + y: "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", + }, { + name: "max uint256 * max uint256", + x: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + y: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, { + name: "alternating bits", + x: "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", + y: "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", + }, { + name: "alternating bits 2", + x: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + y: "5555555555555555555555555555555555555555555555555555555555555555", + }, { + name: "carry propagation through zero limbs", + x: "ffffffffffffffffffffffffffffffff00000000000000000000000000000000", + y: "100000000000000000000000000000001", + }} + + for _, test := range tests { + xBig, yBig := mustBig(test.x), mustBig(test.y) + x, y := mustBigToUint256(xBig), mustBigToUint256(yBig) + want := new(big.Int).Mul(xBig, yBig) + + // Fill the array that will be used to store the product with non-zero + // values to ensure the entire array is overwritten. + var product [8]uint64 + for i := 0; i < 8; i++ { + product[i] = 0xffffffffffffffff + } + + // Compute the product with [Mul512] and ensure it matches the same + // result produced by [big.Int]. + Mul512(&product, &x, &y) + if got := uint512ToBig(product); got.Cmp(want) != 0 { + t.Errorf("%s: incorrect product\n x: %064x\n y: %064x\n"+ + "got: %0128x\nwant: %0128x", test.name, xBig, yBig, got, want) + } + + // Ensure commutativity works properly with [Mul512] by computing the + // product again with the operands swapped. Poison the array used to + // store the product again to ensure the opposite order overwrites the + // entire array too. + for i := 0; i < 8; i++ { + product[i] = 0xffffffffffffffff + } + Mul512(&product, &y, &x) + if got := uint512ToBig(product); got.Cmp(want) != 0 { + t.Errorf("%s (swapped): incorrect product\n y: %064x\n x: %064x\n"+ + "got: %0128x\nwant: %0128x", test.name, yBig, xBig, got, want) + } + } +} + +// TestMul512Random ensures that multiplying randomly-generated values via +// [Mul512] returns the expected result by comparing the product against the +// [big.Int] result. It also tests commutativity. +func TestMul512Random(t *testing.T) { + // Use a unique random seed each test instance and log it if the tests fail. + seed := time.Now().UnixNano() + rng := mrand.New(mrand.NewSource(seed)) + defer func(t *testing.T, seed int64) { + if t.Failed() { + t.Logf("random seed: %d", seed) + } + }(t, seed) + + for i := 0; i < 1000; i++ { + // Generate random [big.Int] operands and the expected product. + xBig, yBig := randBig(t, rng), randBig(t, rng) + x, y := mustBigToUint256(xBig), mustBigToUint256(yBig) + want := new(big.Int).Mul(xBig, yBig) + + // Compute the product with [Mul512] and ensure it matches the same + // result produced by [big.Int]. + var product [8]uint64 + Mul512(&product, &x, &y) + if got := uint512ToBig(product); got.Cmp(want) != 0 { + t.Errorf("incorrect product\n x: %064x\n y: %064x\ngot: %0128x\n"+ + "want: %0128x", xBig, yBig, got, want) + } + + // Ensure commutativity works properly with [Mul512] by computing the + // product again with the operands swapped. + var product2 [8]uint64 + Mul512(&product2, &y, &x) + if got := uint512ToBig(product2); got.Cmp(want) != 0 { + t.Errorf("incorrect product\n y: %064x\n x: %064x\ngot: %0128x\n"+ + "want: %0128x", yBig, xBig, got, want) + } + } +} From 5c1c8cc3877b42ac201080ec2003cd25865e336b Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sun, 26 Jul 2026 05:07:27 -0500 Subject: [PATCH 16/33] secp256k1: Add Square512 tests. This adds dedicated tests for the 512-bit squaring in the new internal arith package. While it is already already well exercised by the upper layers that use it, testing primitives directly helps ensure correctness in isolation. --- dcrec/secp256k1/internal/arith/arith_test.go | 151 +++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/dcrec/secp256k1/internal/arith/arith_test.go b/dcrec/secp256k1/internal/arith/arith_test.go index c2bc42abce..31f9469409 100644 --- a/dcrec/secp256k1/internal/arith/arith_test.go +++ b/dcrec/secp256k1/internal/arith/arith_test.go @@ -242,3 +242,154 @@ func TestMul512Random(t *testing.T) { } } } + +// TestSquare512 ensures [Square512] returns the expected result by comparing it +// against the [big.Int] result. It also tests full buffer replacement and that +// [Mul512] and [Square512] agree on the result. +func TestSquare512(t *testing.T) { + tests := []struct { + name string // test description + a string // hex encoded value to square + }{{ + name: "zero", + a: "0", + }, { + name: "identity", + a: "1", + }, { + name: "small value", + a: "2", + }, { + name: "2^4", + a: "10", + }, { + name: "2^8", + a: "100", + }, { + name: "2^16", + a: "10000", + }, { + name: "2^32", + a: "100000000", + }, { + name: "max uint64", + a: "ffffffffffffffff", + }, { + name: "2^64", + a: "10000000000000000", + }, { + name: "max uint128 minus one", + a: "fffffffffffffffffffffffffffffffe", + }, { + name: "max uint128", + a: "ffffffffffffffffffffffffffffffff", + }, { + name: "2^128", + a: "100000000000000000000000000000000", + }, { + name: "max uint192", + a: "ffffffffffffffffffffffffffffffffffffffffffffffff", + }, { + name: "2^192", + a: "1000000000000000000000000000000000000000000000000", + }, { + name: "2^255", + a: "8000000000000000000000000000000000000000000000000000000000000000", + }, { + name: "near max uint256", + a: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0001", + }, { + name: "max uint256 minus one", + a: "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", + }, { + name: "max uint256", + a: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, { + name: "sparse bits", + a: "8000000000000000000000000000000100000000000000000000000000000001", + }, { + name: "high and low bits", + a: "8000000000000000000000000000000000000000000000000000000000000001", + }, { + name: "alternating bits", + a: "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", + }, { + name: "alternating bits 2", + a: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, { + name: "alternating bits 3", + a: "5555555555555555555555555555555555555555555555555555555555555555", + }, { + name: "carry propagation through zero limbs", + a: "ffffffffffffffffffffffffffffffff00000000000000000000000000000001", + }} + + for _, test := range tests { + aBig := mustBig(test.a) + a := mustBigToUint256(aBig) + want := new(big.Int).Mul(aBig, aBig) + + // Fill the array that will be used to store the result with non-zero + // values to ensure the entire array is overwritten. + var result [8]uint64 + for i := 0; i < 8; i++ { + result[i] = 0xffffffffffffffff + } + + // Compute the result with [Square512] and ensure it matches the same + // result produced by [big.Int]. + Square512(&result, &a) + if got := uint512ToBig(result); got.Cmp(want) != 0 { + t.Errorf("%s: incorrect square\n a: %064x\n got: %0128x\n"+ + "want: %0128x", test.name, aBig, got, want) + } + + // Ensure [Mul512] and [Square512] agree. + var mulResult [8]uint64 + Mul512(&mulResult, &a, &a) + if result != mulResult { + t.Errorf("%s: square and mul disagree\n a: %064x\n"+ + " square: %0128x\n mul: %0128x", test.name, aBig, + uint512ToBig(result), uint512ToBig(mulResult)) + } + } +} + +// TestSquare512Random ensures that squaring randomly-generated values via +// [Square512] returns the expected result by comparing it against the [big.Int] +// result. It also tests that [Mul512] and [Square512] agree on the result. +func TestSquare512Random(t *testing.T) { + // Use a unique random seed each test instance and log it if the tests fail. + seed := time.Now().UnixNano() + rng := mrand.New(mrand.NewSource(seed)) + defer func(t *testing.T, seed int64) { + if t.Failed() { + t.Logf("random seed: %d", seed) + } + }(t, seed) + + for i := 0; i < 1000; i++ { + // Generate a random [big.Int] operand and the expected result. + aBig := randBig(t, rng) + a := mustBigToUint256(aBig) + want := new(big.Int).Mul(aBig, aBig) + + // Compute the result with [Square512] and ensure it matches the same + // result produced by [big.Int]. + var result [8]uint64 + Square512(&result, &a) + if got := uint512ToBig(result); got.Cmp(want) != 0 { + t.Errorf("incorrect square\n a: %064x\n got: %0128x\n"+ + "want: %0128x", aBig, got, want) + } + + // Ensure [Mul512] and [Square512] agree. + var mulResult [8]uint64 + Mul512(&mulResult, &a, &a) + if result != mulResult { + t.Errorf("square and mul disagree\n a: %064x\n"+ + " square: %0128x\n mul: %0128x", aBig, + uint512ToBig(result), uint512ToBig(mulResult)) + } + } +} From d74c9d52dac0681c00c3b627a3476caa6b192e7c Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sun, 26 Jul 2026 01:30:44 -0500 Subject: [PATCH 17/33] secp256k1: Add arith README.md. --- dcrec/secp256k1/internal/arith/README.md | 23 +++++++++++++++++++++++ dcrec/secp256k1/internal/arith/arith.go | 2 ++ 2 files changed, 25 insertions(+) create mode 100644 dcrec/secp256k1/internal/arith/README.md diff --git a/dcrec/secp256k1/internal/arith/README.md b/dcrec/secp256k1/internal/arith/README.md new file mode 100644 index 0000000000..09660b20b7 --- /dev/null +++ b/dcrec/secp256k1/internal/arith/README.md @@ -0,0 +1,23 @@ +arith +===== + +[![Build Status](https://github.com/decred/dcrd/workflows/Build%20and%20Test/badge.svg)](https://github.com/decred/dcrd/actions) +[![ISC License](https://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![Doc](https://img.shields.io/badge/doc-reference-blue.svg)](https://pkg.go.dev/github.com/decred/dcrd/dcrec/secp256k1/v4/internal/arith) + +## Overview + +This provides low-level constant-time primitives and modulus-agnostic arithmetic +shared by multiple implementations in this module. + +See [internal/proofs/README.md](../proofs/README.md) for formal verification of +the 512-bit multiplication and 512-bit squaring. + +## Installation and Updating + +This package is internal and therefore is neither directly installed nor needs +to be manually updated. + +## License + +Package arith is licensed under the [copyfree](http://copyfree.org) ISC License. diff --git a/dcrec/secp256k1/internal/arith/arith.go b/dcrec/secp256k1/internal/arith/arith.go index 13b35d6737..b6e75cd2f2 100644 --- a/dcrec/secp256k1/internal/arith/arith.go +++ b/dcrec/secp256k1/internal/arith/arith.go @@ -2,6 +2,8 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. +// Package arith provides low-level constant-time primitives and +// modulus-agnostic arithmetic. package arith import "math/bits" From 322eb99781fe3880bd18814036724c4fb3705542 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Mon, 27 Jul 2026 02:56:06 -0500 Subject: [PATCH 18/33] secp256k1: Add internal cpufeat package. The current CPU feature detection for the recently added MULX/ADX optimizations is done directly in the field64 code. There is nothing wrong with this from a purely functional standpoint, but it is difficult to extend and can't be reused across other packages in the module. With that in mind, this adds an internal package to detect support for CPU and operating system features that are relevant to architecture-specific optimizations throughout the secp256k1 package. The package intentionally provides a minimal abstraction consisting only of the feature detection required by the secp256k1 implementation. This provides a clean mechanism for detecting and supporting new hardware extensions in the future. Note that does not incorporate the package yet. It only introduces the new code. --- dcrec/secp256k1/internal/cpufeat/cpu.go | 22 +++++ dcrec/secp256k1/internal/cpufeat/cpu_386.s | 49 +++++++++++ dcrec/secp256k1/internal/cpufeat/cpu_amd64.s | 49 +++++++++++ .../secp256k1/internal/cpufeat/cpu_generic.go | 12 +++ dcrec/secp256k1/internal/cpufeat/cpu_x86.go | 86 +++++++++++++++++++ dcrec/secp256k1/internal/cpufeat/cpu_x86.s | 21 +++++ 6 files changed, 239 insertions(+) create mode 100644 dcrec/secp256k1/internal/cpufeat/cpu.go create mode 100644 dcrec/secp256k1/internal/cpufeat/cpu_386.s create mode 100644 dcrec/secp256k1/internal/cpufeat/cpu_amd64.s create mode 100644 dcrec/secp256k1/internal/cpufeat/cpu_generic.go create mode 100644 dcrec/secp256k1/internal/cpufeat/cpu_x86.go create mode 100644 dcrec/secp256k1/internal/cpufeat/cpu_x86.s diff --git a/dcrec/secp256k1/internal/cpufeat/cpu.go b/dcrec/secp256k1/internal/cpufeat/cpu.go new file mode 100644 index 0000000000..3ad7cbad3c --- /dev/null +++ b/dcrec/secp256k1/internal/cpufeat/cpu.go @@ -0,0 +1,22 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +// Package cpufeat provides detection of CPU support for relevant hardware +// features. +package cpufeat + +// features caches the result of querying the CPU for supported features. +var features = detect() + +// Supported returns the feature set detected during package initialization. +func Supported() Features { + return features +} + +// Features houses flags that specify whether or not various features +// are supported by the CPU. +type Features struct { + BMI2 bool + ADX bool +} diff --git a/dcrec/secp256k1/internal/cpufeat/cpu_386.s b/dcrec/secp256k1/internal/cpufeat/cpu_386.s new file mode 100644 index 0000000000..f1e6b34d3a --- /dev/null +++ b/dcrec/secp256k1/internal/cpufeat/cpu_386.s @@ -0,0 +1,49 @@ +// Copyright (c) 2024-2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. +// +// Feature detection originally written by Dave Collins Feb 2019. Modifications +// for secp256k1 made in Jul 2026. + +//go:build !purego + +#include "textflag.h" + +// func supportsCPUID() bool +TEXT ·supportsCPUID(SB), NOSPLIT, $0-1 + // Per the Intel 64 and IA-32 Architectures Software Developer's Manual, + // CPUID is supported if bit 21 of the EFLAGS register can be modified. + // + // To that end, this works as follows: + // + // 1. Get the current value of EFLAGS by pushing it and popping it into AX. + // 2. Make a copy into BX for later comparison. + // 3. Toggle bit 21 (the EFLAGS ID bit) of AX. + // 4. Put the modified value back into EFLAGS by pushing it and popping it + // into EFLAGS. The CPU will either update bit 21 of the EFLAGS with the + // modified value when it supports CPUID or leave it unmodified when it + // does not. + // 5. Get the potentially modified value of EFLAGS by pushing it and popping + // it into AX. + // 6. Put the original value back into EFLAGS to avoid any observable side + // effects by pushing it and popping it into EFLAGS. + // 7. Compare the original and potentially modified EFLAGS (aka AX vs BX) + // 8. CPUID is supported when they do not match since bit 21 was able to be + // modified. + PUSHFL + POPL AX + MOVL AX, BX + XORL $0x200000, AX + PUSHL AX + POPFL + PUSHFL + POPL AX + PUSHL BX + POPFL + CMPL AX, BX + JE nocpuid + MOVB $1, ret+0(FP) + RET +nocpuid: + MOVB $0, ret+0(FP) + RET diff --git a/dcrec/secp256k1/internal/cpufeat/cpu_amd64.s b/dcrec/secp256k1/internal/cpufeat/cpu_amd64.s new file mode 100644 index 0000000000..678f442fc2 --- /dev/null +++ b/dcrec/secp256k1/internal/cpufeat/cpu_amd64.s @@ -0,0 +1,49 @@ +// Copyright (c) 2024-2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. +// +// Feature detection originally written by Dave Collins Feb 2019. Modifications +// for secp256k1 made in Jul 2026. + +//go:build !purego + +#include "textflag.h" + +// func supportsCPUID() bool +TEXT ·supportsCPUID(SB), NOSPLIT, $0-1 + // Per the Intel 64 and IA-32 Architectures Software Developer's Manual, + // CPUID is supported if bit 21 of the EFLAGS register can be modified. + // + // To that end, this works as follows: + // + // 1. Get the current value of EFLAGS by pushing it and popping it into AX. + // 2. Make a copy into BX for later comparison. + // 3. Toggle bit 21 (the EFLAGS ID bit) of AX. + // 4. Put the modified value back into EFLAGS by pushing it and popping it + // into EFLAGS. The CPU will either update bit 21 of the EFLAGS with the + // modified value when it supports CPUID or leave it unmodified when it + // does not. + // 5. Get the potentially modified value of EFLAGS by pushing it and popping + // it into AX. + // 6. Put the original value back into EFLAGS to avoid any observable side + // effects by pushing it and popping it into EFLAGS. + // 7. Compare the original and potentially modified EFLAGS (aka AX vs BX) + // 8. CPUID is supported when they do not match since bit 21 was able to be + // modified. + PUSHFQ + POPQ AX + MOVQ AX, BX + XORQ $0x200000, AX + PUSHQ AX + POPFQ + PUSHFQ + POPQ AX + PUSHQ BX + POPFQ + CMPQ AX, BX + JE nocpuid + MOVB $1, ret+0(FP) + RET +nocpuid: + MOVB $0, ret+0(FP) + RET diff --git a/dcrec/secp256k1/internal/cpufeat/cpu_generic.go b/dcrec/secp256k1/internal/cpufeat/cpu_generic.go new file mode 100644 index 0000000000..18852d31bf --- /dev/null +++ b/dcrec/secp256k1/internal/cpufeat/cpu_generic.go @@ -0,0 +1,12 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +//go:build !(386 || amd64) || purego + +package cpufeat + +// detect returns false for all features by default. +func detect() Features { + return Features{} +} diff --git a/dcrec/secp256k1/internal/cpufeat/cpu_x86.go b/dcrec/secp256k1/internal/cpufeat/cpu_x86.go new file mode 100644 index 0000000000..69633495ea --- /dev/null +++ b/dcrec/secp256k1/internal/cpufeat/cpu_x86.go @@ -0,0 +1,86 @@ +// Copyright (c) 2024-2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. +// +// Feature detection originally written by Dave Collins Feb 2019 for blake256. +// Additional cleanup and comments added Jul 2024. Modifications for secp256k1 +// made in Jul 2026. + +//go:build (386 || amd64) && !purego + +package cpufeat + +// supportsCPUID returns true when the CPU supports the CPUID opcode. +// +//go:noescape +func supportsCPUID() bool + +// cpuid provides access to the CPUID opcode. +// +//go:noescape +func cpuid(eaxIn, ecxIn uint32) (eax, ebx, ecx, edx uint32) + +// hasBit returns whether or not the provided bit is set in the given test +// value. +func hasBit(testVal uint32, bit uint) bool { + return testVal>>bit&1 == 1 +} + +// detect returns the result of querying the CPU to determine supported +// features. +func detect() Features { + // Per CPUID—CPU Identification in Chapter 3 of the Intel 64 and IA-32 + // Architectures Software Developer's Manual, Volume 2A: + // + // "The ID flag (bit 21) in the EFLAGS register indicates support for the + // CPUID instruction. If a software procedure can set and clear this flag, + // the processor executing the procedure supports the CPUID instruction. + // This instruction operates the same in non-64-bit modes and 64-bit mode. + // + // CPUID returns processor identification and feature information in the + // EAX, EBX, ECX, and EDX registers. The output is dependent on the + // contents of the EAX register upon execution (in some cases, ECX as + // well)." + // + // The inputs and outputs for determining various levels of support that are + // relevant to secp256k1 are: + // + // Initial EAX Value | Output + // ------------------|------------------------------------------------ + // 0x00 | EAX = Maximum Input Value for Basic CPUID Info. + // ------------------------------------------------------------------- + // 0x07 | EBX = Feature Information + // | Bit 8 = Bit Manipulation Instruction Set 2 (BMI2) + // | Bit 19 = Multi-Precision Add-Carry Extensions (ADX) + const ( + eaxInputQueryMax = 0x00 + eaxInputQueryExtFeatFlags = 0x07 + + ebx7OutputBMI2Bit = 8 + ebx7OutputADXBit = 19 + ) + + // Nothing to do if the CPU somehow does not support CPUID. Go probably + // won't even run on such a CPU, but as the Intel manual states, it is + // technically required to check if CPUID is supported before querying it + // and it's best to be safe. + var f Features + if !supportsCPUID() { + return f + } + + // Querying the supported feature info for BMI2 and ADX is only valid if the + // CPU at least supports querying the Structured Extended Feature + // Enumeration sub-leaf. + maxEAXInput, _, _, _ := cpuid(eaxInputQueryMax, 0) + if maxEAXInput < eaxInputQueryExtFeatFlags { + return f + } + + // Query extended feature info to determine BMI2 and ADX support. + _, ebx, _, _ := cpuid(eaxInputQueryExtFeatFlags, 0) + f.BMI2 = hasBit(ebx, ebx7OutputBMI2Bit) + f.ADX = hasBit(ebx, ebx7OutputADXBit) + + return f +} diff --git a/dcrec/secp256k1/internal/cpufeat/cpu_x86.s b/dcrec/secp256k1/internal/cpufeat/cpu_x86.s new file mode 100644 index 0000000000..0237b3cf73 --- /dev/null +++ b/dcrec/secp256k1/internal/cpufeat/cpu_x86.s @@ -0,0 +1,21 @@ +// Copyright (c) 2024-2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. +// +// Feature detection originally written by Dave Collins Feb 2019. Modifications +// for secp256k1 made in Jul 2026. + +//go:build (386 || amd64) && !purego + +#include "textflag.h" + +// func cpuid(eaxIn, ecxIn uint32) (eax, ebx, ecx, edx uint32) +TEXT ·cpuid(SB), NOSPLIT, $0-24 + MOVL eaxIn+0(FP), AX + MOVL ecxIn+4(FP), CX + CPUID + MOVL AX, eax+8(FP) + MOVL BX, ebx+12(FP) + MOVL CX, ecx+16(FP) + MOVL DX, edx+20(FP) + RET From 83f3a54eb7f55164223f42531dff984a461091e7 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Mon, 27 Jul 2026 04:01:12 -0500 Subject: [PATCH 19/33] secp256k1: Add cpufeat tests. This adds dedicated tests for the functions in the new internal cpufeat package. Since the specific values depend on the hardware, this avoids asserting specific values that would fail when not running on specific hardware and instead focuses on ensuring the functions are idempotent. --- .../internal/cpufeat/cpu_generic_test.go | 20 ++++ dcrec/secp256k1/internal/cpufeat/cpu_test.go | 28 +++++ .../internal/cpufeat/cpu_x86_test.go | 111 ++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 dcrec/secp256k1/internal/cpufeat/cpu_generic_test.go create mode 100644 dcrec/secp256k1/internal/cpufeat/cpu_test.go create mode 100644 dcrec/secp256k1/internal/cpufeat/cpu_x86_test.go diff --git a/dcrec/secp256k1/internal/cpufeat/cpu_generic_test.go b/dcrec/secp256k1/internal/cpufeat/cpu_generic_test.go new file mode 100644 index 0000000000..3519650823 --- /dev/null +++ b/dcrec/secp256k1/internal/cpufeat/cpu_generic_test.go @@ -0,0 +1,20 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +//go:build !(386 || amd64) || purego + +package cpufeat + +import "testing" + +// TestDetectGeneric ensures that the generic feature detection fallback used +// for architectures without dedicated support (and the purego build tag) +// correctly reports that no optional features are supported. +func TestDetectGeneric(t *testing.T) { + want := Features{} + if got := detect(); got != want { + t.Fatalf("unexpected detected features -- got %+v, want %+v", got, + want) + } +} diff --git a/dcrec/secp256k1/internal/cpufeat/cpu_test.go b/dcrec/secp256k1/internal/cpufeat/cpu_test.go new file mode 100644 index 0000000000..78c8d246c9 --- /dev/null +++ b/dcrec/secp256k1/internal/cpufeat/cpu_test.go @@ -0,0 +1,28 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package cpufeat + +import "testing" + +// TestSupported ensures that querying the cached feature set is stable and +// idempotent across repeated calls since it is only ever computed once during +// package initialization. It also ensures that invoking detect directly +// produces the same result. +func TestSupported(t *testing.T) { + got1 := Supported() + got2 := Supported() + if got1 != got2 { + t.Fatalf("inconsistent results across calls -- got %+v and %+v", got1, + got2) + } + + // Ensure the cached features exposed via [Supported] match since they are + // derived at package initialization time. + want := detect() + if got := Supported(); got != want { + t.Fatalf("unexpected supported features -- got %+v, want %+v", got, + want) + } +} diff --git a/dcrec/secp256k1/internal/cpufeat/cpu_x86_test.go b/dcrec/secp256k1/internal/cpufeat/cpu_x86_test.go new file mode 100644 index 0000000000..177ebe707f --- /dev/null +++ b/dcrec/secp256k1/internal/cpufeat/cpu_x86_test.go @@ -0,0 +1,111 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +//go:build (386 || amd64) && !purego + +package cpufeat + +import "testing" + +// TestHasBit ensures that determining whether a specific bit is set in a 32-bit +// test value works as expected for edge cases, including the exact bit +// positions used to detect BMI2 and ADX. +func TestHasBit(t *testing.T) { + tests := []struct { + name string // test description + testVal uint32 // value to test + bit uint // bit position to check + want bool // expected result + }{{ + name: "bit 0 set", + testVal: 0x00000001, + bit: 0, + want: true, + }, { + name: "bit 0 unset", + testVal: 0x00000000, + bit: 0, + want: false, + }, { + name: "bit 8 set (BMI2 position in CPUID leaf 7 EBX)", + testVal: 0x00000100, + bit: 8, + want: true, + }, { + name: "bit 8 unset with all other bits set", + testVal: 0xfffffeff, + bit: 8, + want: false, + }, { + name: "bit 19 set (ADX position in CPUID leaf 7 EBX)", + testVal: 0x00080000, + bit: 19, + want: true, + }, { + name: "bit 19 unset with all other bits set", + testVal: 0xfff7ffff, + bit: 19, + want: false, + }, { + name: "bit 31 set (highest bit)", + testVal: 0x80000000, + bit: 31, + want: true, + }, { + name: "bit 31 unset with all other bits set", + testVal: 0x7fffffff, + bit: 31, + want: false, + }, { + name: "all bits set, check an arbitrary middle bit", + testVal: 0xffffffff, + bit: 15, + want: true, + }, { + name: "no bits set, check an arbitrary middle bit", + testVal: 0x00000000, + bit: 15, + want: false, + }} + + for _, test := range tests { + got := hasBit(test.testVal, test.bit) + if got != test.want { + t.Errorf("%s: unexpected result -- got %v, want %v", test.name, + got, test.want) + } + } +} + +// TestSupportsCPUID ensures that querying whether the CPU supports the CPUID +// opcode returns an idempotent result across repeated calls. +// +// Note that this intentionally does not assert a specific value since the +// result is hardware dependent. In practice, every CPU capable of running +// this compiled test binary supports CPUID, but the point of this test is to +// catch a broken implementation that behaves inconsistently rather than to +// assert a particular machine's capabilities. +func TestSupportsCPUID(t *testing.T) { + got1 := supportsCPUID() + got2 := supportsCPUID() + if got1 != got2 { + t.Fatalf("returned inconsistent results across calls -- got %v vs %v", + got1, got2) + } +} + +// TestCPUID ensures that querying CPUID with the same inputs consistently +// returns the same outputs. +func TestCPUID(t *testing.T) { + // The maximum supported input value leaf (EAX=0) is queried since it is the + // first leaf queried by [detect] itself which makes it reasonable for + // exercising the primitive directly. + eax1, ebx1, ecx1, edx1 := cpuid(0, 0) + eax2, ebx2, ecx2, edx2 := cpuid(0, 0) + if eax1 != eax2 || ebx1 != ebx2 || ecx1 != ecx2 || edx1 != edx2 { + t.Fatalf("cpuid returned inconsistent results across calls -- got "+ + "(%x,%x,%x,%x) vs (%x,%x,%x,%x)", eax1, ebx1, ecx1, edx1, eax2, + ebx2, ecx2, edx2) + } +} From 2a6cfa1052af7d89383580cf48d513182794e663 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Mon, 27 Jul 2026 03:06:23 -0500 Subject: [PATCH 20/33] secp256k1: Add cpufeat README.md. --- dcrec/secp256k1/internal/cpufeat/README.md | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 dcrec/secp256k1/internal/cpufeat/README.md diff --git a/dcrec/secp256k1/internal/cpufeat/README.md b/dcrec/secp256k1/internal/cpufeat/README.md new file mode 100644 index 0000000000..e03f2d9e51 --- /dev/null +++ b/dcrec/secp256k1/internal/cpufeat/README.md @@ -0,0 +1,52 @@ +cpufeat +======= + +[![Build Status](https://github.com/decred/dcrd/workflows/Build%20and%20Test/badge.svg)](https://github.com/decred/dcrd/actions) +[![ISC License](https://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![Doc](https://img.shields.io/badge/doc-reference-blue.svg)](https://pkg.go.dev/github.com/decred/dcrd/dcrec/secp256k1/v4/internal/cpufeat) + +Package cpufeat detects support for CPU features that are relevant to +architecture-specific optimizations throughout the `secp256k1` package. + +## Design + +The package intentionally provides a minimal abstraction consisting only of the +feature detection required by the `secp256k1` implementation. Architecture- +specific detection logic is gated behind build constraints and unsupported +architectures simply report that no optional features are supported. + +This organization allows optimized implementations to select the most efficient +available code path without duplicating processor detection logic throughout the +codebase and ensures portable fallback implementations remain available on all +supported platforms. + +## Pure Go Builds + +The `purego` build tag may be used to disable all assembly code in this package. +Since feature detection relies on assembly, all optional CPU features will +report as unsupported. + +## Testing + +It is possible to test implementations that require otherwise unavailable CPU +features by using software such as the [Intel Software Development +Emulator](https://www.intel.com/content/www/us/en/developer/articles/tool/software-development-emulator.html). + +Some relevant flags for testing purposes with the Intel SDE are: + +* BMI2: `-hsw Set chip-check and CPUID for Intel(R) Haswell CPU` +* ADX: `-bdw Set chip-check and CPUID for Intel(R) Broadwell CPU` + +The package determines supported features during package initialization, so +tests should be run under the emulator rather than attempting to enable features +after startup. + +## Installation and Updating + +This package is internal and therefore is neither directly installed nor needs +to be manually updated. + +## License + +Package cpufeat is licensed under the [copyfree](http://copyfree.org) ISC +License. From f266edfafc08a5f579452eab1817c13d75408ae5 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Mon, 27 Jul 2026 03:06:58 -0500 Subject: [PATCH 21/33] secp256k1: Use the new internal cpufeat package. This updates the field64 code to use the new cpufeat package for feature detection instead of doing it manually inline. --- dcrec/secp256k1/field64_amd64.go | 40 +++++++-------------- dcrec/secp256k1/field64_amd64.s | 11 ------ dcrec/secp256k1/field64_bench_amd64_test.go | 4 +-- 3 files changed, 14 insertions(+), 41 deletions(-) diff --git a/dcrec/secp256k1/field64_amd64.go b/dcrec/secp256k1/field64_amd64.go index bef8f12cc2..f76ff58291 100644 --- a/dcrec/secp256k1/field64_amd64.go +++ b/dcrec/secp256k1/field64_amd64.go @@ -6,27 +6,16 @@ package secp256k1 -import "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/arith" - -// field64UseADX reports whether the CPU supports both BMI2 (MULX) and ADX (ADCX/ADOX). -var field64UseADX = func() bool { - const ( - eaxMax = 0 - eaxExt = 7 - bmi2Bit = 8 - adxBit = 19 - ) - - // Leaf 0x07 is only valid when the CPU reports it within the max input. - eax, _, _, _ := field64CPUID(eaxMax, 0) - if eax < eaxExt { - return false - } - - _, ebx, _, _ := field64CPUID(eaxExt, 0) - hasBMI2 := ebx>>bmi2Bit&1 == 1 - hasADX := ebx>>adxBit&1 == 1 - return hasBMI2 && hasADX +import ( + "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/arith" + "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/cpufeat" +) + +// field64UseBMI2AndADX is enabled when the CPU supports both BMI2 (MULX) and +// ADX (ADCX/ADOX). +var field64UseBMI2AndADX = func() bool { + f := cpufeat.Supported() + return f.BMI2 && f.ADX }() //go:noescape @@ -35,14 +24,9 @@ func field64MulADX(r *[4]uint64, a, b *[4]uint64) //go:noescape func field64SquareADX(r *[4]uint64, a *[4]uint64) -// field64CPUID provides access to the CPUID opcode. -// -//go:noescape -func field64CPUID(eaxIn, ecxIn uint32) (eax, ebx, ecx, edx uint32) - // field64Mul sets r = a * b (mod p) func field64Mul(r *[4]uint64, a, b *[4]uint64) { - if field64UseADX { + if field64UseBMI2AndADX { field64MulADX(r, a, b) return } @@ -51,7 +35,7 @@ func field64Mul(r *[4]uint64, a, b *[4]uint64) { // field64Square sets r = a^2 (mod p) func field64Square(r *[4]uint64, a *[4]uint64) { - if field64UseADX { + if field64UseBMI2AndADX { field64SquareADX(r, a) return } diff --git a/dcrec/secp256k1/field64_amd64.s b/dcrec/secp256k1/field64_amd64.s index aeb851ae5d..23e12aeb97 100644 --- a/dcrec/secp256k1/field64_amd64.s +++ b/dcrec/secp256k1/field64_amd64.s @@ -188,14 +188,3 @@ TEXT ·field64SquareADX(SB), NOSPLIT, $0-16 REDUCE() RET - -// func field64CPUID(eaxIn, ecxIn uint32) (eax, ebx, ecx, edx uint32) -TEXT ·field64CPUID(SB), NOSPLIT, $0-24 - MOVL eaxIn+0(FP), AX - MOVL ecxIn+4(FP), CX - CPUID - MOVL AX, eax+8(FP) - MOVL BX, ebx+12(FP) - MOVL CX, ecx+16(FP) - MOVL DX, edx+20(FP) - RET diff --git a/dcrec/secp256k1/field64_bench_amd64_test.go b/dcrec/secp256k1/field64_bench_amd64_test.go index adb4c5225c..42953760fa 100644 --- a/dcrec/secp256k1/field64_bench_amd64_test.go +++ b/dcrec/secp256k1/field64_bench_amd64_test.go @@ -22,7 +22,7 @@ func BenchmarkField64MulAMD64(b *testing.B) { field64MulGeneric(&r, &a, &c) } }) - if field64UseADX { + if field64UseBMI2AndADX { b.Run("MULX/ADX", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() @@ -46,7 +46,7 @@ func BenchmarkField64SquareAMD64(b *testing.B) { field64SquareGeneric(&r, &a) } }) - if field64UseADX { + if field64UseBMI2AndADX { b.Run("MULX/ADX", func(b *testing.B) { b.ReportAllocs() b.ResetTimer() From 12e950df40ca593aede49c7dea59f3e5d9645806 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Mon, 27 Jul 2026 15:53:08 -0500 Subject: [PATCH 22/33] secp256k1: Cleanup field64 benchmarks. This modifies the field64 amd64 benchmarks to be more consistent with the approach used for architecture-specific benchmarks in the rest of the code base. This approach is cleaner and makes it easier to add additional specialized implementations in the future. --- dcrec/secp256k1/field64_bench_amd64_test.go | 76 ++++++++++++++------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/dcrec/secp256k1/field64_bench_amd64_test.go b/dcrec/secp256k1/field64_bench_amd64_test.go index 42953760fa..3c32eee0c6 100644 --- a/dcrec/secp256k1/field64_bench_amd64_test.go +++ b/dcrec/secp256k1/field64_bench_amd64_test.go @@ -10,51 +10,75 @@ import ( "testing" ) -func BenchmarkField64MulAMD64(b *testing.B) { +// BenchmarkField64MulReduceAMD64 benchmarks how long it takes to multiply two +// field elements together and reduce them modulo the field prime with each of +// the specialized amd64 implementations along with the number of allocations +// needed. +func BenchmarkField64MulReduceAMD64(b *testing.B) { + benches := []struct { + name string + fn func(r *[4]uint64, a, b *[4]uint64) + supported bool + }{ + {name: "Generic", fn: field64MulGeneric, supported: true}, + {name: "BMI2/ADX", fn: field64MulADX, supported: field64UseBMI2AndADX}, + } + a := mustFieldVal64("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab").n c := mustFieldVal64("16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca").n var r [4]uint64 - b.Run("Generic", func(b *testing.B) { - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - field64MulGeneric(&r, &a, &c) - } - }) - if field64UseBMI2AndADX { - b.Run("MULX/ADX", func(b *testing.B) { + for _, bench := range benches { + b.Run(bench.name, func(b *testing.B) { + if !bench.supported { + // Note that this is intentionally not using b.Skipf because + // tinygo does not support it. + b.Logf("Skipping %s bench (disabled or no instruction set "+ + "support)", bench.name) + return + } + b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - field64MulADX(&r, &a, &c) + bench.fn(&r, &a, &c) } }) - } else { - b.Log("Skipping MULX/ADX bench (disabled or no instruction set support)") } } -func BenchmarkField64SquareAMD64(b *testing.B) { +// BenchmarkField64SquareReduceAMD64 benchmarks how long it takes to square a +// field element and reduce it modulo the field prime with each of the +// specialized amd64 implementations along with the number of allocations +// needed. +func BenchmarkField64SquareReduceAMD64(b *testing.B) { + benches := []struct { + name string + fn func(r *[4]uint64, a *[4]uint64) + supported bool + }{ + {name: "Generic", fn: field64SquareGeneric, supported: true}, + {name: "BMI2/ADX", fn: field64SquareADX, supported: field64UseBMI2AndADX}, + } + a := mustFieldVal64("16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca").n var r [4]uint64 - b.Run("Generic", func(b *testing.B) { - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - field64SquareGeneric(&r, &a) - } - }) - if field64UseBMI2AndADX { - b.Run("MULX/ADX", func(b *testing.B) { + for _, bench := range benches { + b.Run(bench.name, func(b *testing.B) { + if !bench.supported { + // Note that this is intentionally not using b.Skipf because + // tinygo does not support it. + b.Logf("Skipping %s bench (disabled or no instruction set "+ + "support)", bench.name) + return + } + b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - field64SquareADX(&r, &a) + bench.fn(&r, &a) } }) - } else { - b.Log("Skipping MULX/ADX bench (disabled or no instruction set support)") } } From 69e01618075bf3c70f750acb4d9af0051e27c3b4 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Mon, 27 Jul 2026 15:53:16 -0500 Subject: [PATCH 23/33] secp256k1: Rename field64Mul/Square funcs. This renames the field64Mul and field64Square funcs and their variants to include Reduce in their name to make it clear they are fused multiply/square-and-reduce operations unlike the generic arith.Mul512 and field64Reduce512 primitives. --- dcrec/secp256k1/field64.go | 4 +-- dcrec/secp256k1/field64_amd64.go | 28 ++++++++++----------- dcrec/secp256k1/field64_amd64.s | 8 +++--- dcrec/secp256k1/field64_bench_amd64_test.go | 8 +++--- dcrec/secp256k1/field64_generic.go | 8 +++--- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/dcrec/secp256k1/field64.go b/dcrec/secp256k1/field64.go index 3968ca6ea6..55e05f8285 100644 --- a/dcrec/secp256k1/field64.go +++ b/dcrec/secp256k1/field64.go @@ -478,7 +478,7 @@ func (f *FieldVal64) Mul(val *FieldVal64) *FieldVal64 { // The field value is returned to support chaining. This enables syntax like: // f3.Mul2(f, f2).AddInt(1) so that f3 = (f * f2) + 1. func (f *FieldVal64) Mul2(a, b *FieldVal64) *FieldVal64 { - field64Mul(&f.n, &a.n, &b.n) + field64MulReduce(&f.n, &a.n, &b.n) return f } @@ -649,7 +649,7 @@ func (f *FieldVal64) Square() *FieldVal64 { // The field value is returned to support chaining. This enables syntax like: // f3.SquareVal(f).Mul(f) so that f3 = f^2 * f = f^3. func (f *FieldVal64) SquareVal(val *FieldVal64) *FieldVal64 { - field64Square(&f.n, &val.n) + field64SquareReduce(&f.n, &val.n) return f } diff --git a/dcrec/secp256k1/field64_amd64.go b/dcrec/secp256k1/field64_amd64.go index f76ff58291..e913c1fbd8 100644 --- a/dcrec/secp256k1/field64_amd64.go +++ b/dcrec/secp256k1/field64_amd64.go @@ -19,38 +19,38 @@ var field64UseBMI2AndADX = func() bool { }() //go:noescape -func field64MulADX(r *[4]uint64, a, b *[4]uint64) +func field64MulReduceADX(r *[4]uint64, a, b *[4]uint64) //go:noescape -func field64SquareADX(r *[4]uint64, a *[4]uint64) +func field64SquareReduceADX(r *[4]uint64, a *[4]uint64) -// field64Mul sets r = a * b (mod p) -func field64Mul(r *[4]uint64, a, b *[4]uint64) { +// field64MulReduce sets r = a * b (mod p) +func field64MulReduce(r *[4]uint64, a, b *[4]uint64) { if field64UseBMI2AndADX { - field64MulADX(r, a, b) + field64MulReduceADX(r, a, b) return } - field64MulGeneric(r, a, b) + field64MulReduceGeneric(r, a, b) } -// field64Square sets r = a^2 (mod p) -func field64Square(r *[4]uint64, a *[4]uint64) { +// field64SquareReduce sets r = a^2 (mod p) +func field64SquareReduce(r *[4]uint64, a *[4]uint64) { if field64UseBMI2AndADX { - field64SquareADX(r, a) + field64SquareReduceADX(r, a) return } - field64SquareGeneric(r, a) + field64SquareReduceGeneric(r, a) } -// field64MulGeneric sets r = a * b (mod p) -func field64MulGeneric(r *[4]uint64, a, b *[4]uint64) { +// field64MulReduceGeneric sets r = a * b (mod p) +func field64MulReduceGeneric(r *[4]uint64, a, b *[4]uint64) { var product [8]uint64 arith.Mul512(&product, a, b) field64Reduce512(r, &product) } -// field64SquareGeneric sets r = a^2 (mod p) -func field64SquareGeneric(r *[4]uint64, a *[4]uint64) { +// field64SquareReduceGeneric sets r = a^2 (mod p) +func field64SquareReduceGeneric(r *[4]uint64, a *[4]uint64) { var product [8]uint64 arith.Square512(&product, a) field64Reduce512(r, &product) diff --git a/dcrec/secp256k1/field64_amd64.s b/dcrec/secp256k1/field64_amd64.s index 23e12aeb97..4d5bbd7dd9 100644 --- a/dcrec/secp256k1/field64_amd64.s +++ b/dcrec/secp256k1/field64_amd64.s @@ -58,8 +58,8 @@ MOVQ R14, 16(AX) \ MOVQ R15, 24(AX) -// func field64MulADX(r *[4]uint64, a, b *[4]uint64) -TEXT ·field64MulADX(SB), NOSPLIT, $0-24 +// func field64MulReduceADX(r *[4]uint64, a, b *[4]uint64) +TEXT ·field64MulReduceADX(SB), NOSPLIT, $0-24 MOVQ a+8(FP), SI MOVQ b+16(FP), DI XORQ BX, BX @@ -126,8 +126,8 @@ TEXT ·field64MulADX(SB), NOSPLIT, $0-24 REDUCE() RET -// func field64SquareADX(r *[4]uint64, a *[4]uint64) -TEXT ·field64SquareADX(SB), NOSPLIT, $0-16 +// func field64SquareReduceADX(r *[4]uint64, a *[4]uint64) +TEXT ·field64SquareReduceADX(SB), NOSPLIT, $0-16 MOVQ a+8(FP), AX MOVQ 0(AX), DX // a0 MOVQ 8(AX), SI // a1 diff --git a/dcrec/secp256k1/field64_bench_amd64_test.go b/dcrec/secp256k1/field64_bench_amd64_test.go index 3c32eee0c6..5899576b48 100644 --- a/dcrec/secp256k1/field64_bench_amd64_test.go +++ b/dcrec/secp256k1/field64_bench_amd64_test.go @@ -20,8 +20,8 @@ func BenchmarkField64MulReduceAMD64(b *testing.B) { fn func(r *[4]uint64, a, b *[4]uint64) supported bool }{ - {name: "Generic", fn: field64MulGeneric, supported: true}, - {name: "BMI2/ADX", fn: field64MulADX, supported: field64UseBMI2AndADX}, + {name: "Generic", fn: field64MulReduceGeneric, supported: true}, + {name: "BMI2/ADX", fn: field64MulReduceADX, supported: field64UseBMI2AndADX}, } a := mustFieldVal64("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab").n @@ -57,8 +57,8 @@ func BenchmarkField64SquareReduceAMD64(b *testing.B) { fn func(r *[4]uint64, a *[4]uint64) supported bool }{ - {name: "Generic", fn: field64SquareGeneric, supported: true}, - {name: "BMI2/ADX", fn: field64SquareADX, supported: field64UseBMI2AndADX}, + {name: "Generic", fn: field64SquareReduceGeneric, supported: true}, + {name: "BMI2/ADX", fn: field64SquareReduceADX, supported: field64UseBMI2AndADX}, } a := mustFieldVal64("16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca").n diff --git a/dcrec/secp256k1/field64_generic.go b/dcrec/secp256k1/field64_generic.go index 6ad4be30a7..15ee98add8 100644 --- a/dcrec/secp256k1/field64_generic.go +++ b/dcrec/secp256k1/field64_generic.go @@ -8,15 +8,15 @@ package secp256k1 import "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/arith" -// field64Mul sets r = a * b (mod p). -func field64Mul(r *[4]uint64, a, b *[4]uint64) { +// field64MulReduce sets r = a * b (mod p). +func field64MulReduce(r *[4]uint64, a, b *[4]uint64) { var product [8]uint64 arith.Mul512(&product, a, b) field64Reduce512(r, &product) } -// field64Square sets r = a^2 (mod p). -func field64Square(r *[4]uint64, a *[4]uint64) { +// field64SquareReduce sets r = a^2 (mod p). +func field64SquareReduce(r *[4]uint64, a *[4]uint64) { var product [8]uint64 arith.Square512(&product, a) field64Reduce512(r, &product) From 2699750ce61e5553994beaca9ec47064dcb31607 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Mon, 27 Jul 2026 15:53:18 -0500 Subject: [PATCH 24/33] secp256k1: Separate generic field64 impls. The generic implementations of the fused field multiplication and squaring operations are currently duplicated in the amd64-specific code and the generic fallback code. Rather than duplicate that code, this moves the shared reduction implementation and generic fused multiply/square helpers into field64_generic.go and updates the architecture-specific dispatch to invoke the shared generic implementation when the optimized implementation is unavailable. To accomplish that, a new field64_noasm.go is introduced that only builds when the architecture-specific code is not built. Finally, it moves the amd64 instruction set architecture dispatch into field64isa_amd64.go file so that it only builds on amd64 and clearly separates ISA selection from the implementation itself. These modifications mirror the approach used for architecture-dependent code in the rest of the code base. --- dcrec/secp256k1/field64.go | 95 ----------------------- dcrec/secp256k1/field64_amd64.go | 35 +-------- dcrec/secp256k1/field64_generic.go | 115 ++++++++++++++++++++++++++-- dcrec/secp256k1/field64_noasm.go | 19 +++++ dcrec/secp256k1/field64isa_amd64.go | 27 +++++++ 5 files changed, 155 insertions(+), 136 deletions(-) create mode 100644 dcrec/secp256k1/field64_noasm.go create mode 100644 dcrec/secp256k1/field64isa_amd64.go diff --git a/dcrec/secp256k1/field64.go b/dcrec/secp256k1/field64.go index 55e05f8285..e6bdef1ffd 100644 --- a/dcrec/secp256k1/field64.go +++ b/dcrec/secp256k1/field64.go @@ -653,101 +653,6 @@ func (f *FieldVal64) SquareVal(val *FieldVal64) *FieldVal64 { return f } -// field64Reduce512 reduces a 512-bit little-endian limb array modulo p in -// constant time and stores the result in r. -func field64Reduce512(r *[4]uint64, x *[8]uint64) { - // This algorithm has been formally verified, including its intermediate - // bounds, carry assumptions, and functional correctness. The verification - // artifacts are available in internal/proofs. - - // Per [HAC] section 14.3.4: Reduction method of moduli of special form, - // when the modulus is of the special form m = b^t - c, highly efficient - // reduction can be achieved. While [HAC] only presents the algorithm and - // does not call it out by name or provide the mathematical justification, - // the underlying technique is known as Crandall reduction and is often - // presented as 2^k - c. It is easy to see they are equivalent by setting - // b = 2 and t = k. - // - // The secp256k1 prime is 2^256 - 4294968273, so it fits this criteria where - // k=256, and c = 4294968273 = 2^32 + 977. - // - // Crandall reduction works by taking advantage of the fact that if a prime - // is of the form 2^k - c, then 2^k - c ≡ 0 (mod p), so 2^k ≡ c (mod p). In - // other words, every multiple of 2^k is equivalent to adding c when working - // modulo p. - // - // Since the 512-bit value to reduce is tightly packed into uint64s, the - // upper 4 limbs are all multiples of 2^256. Therefore, reducing modulo the - // prime is equivalent to multiplying those upper limbs by c and adding the - // result to the corresponding lower 4 limbs while propagating the carries. - // - // For the specific case of the secp256k1 prime, a max of 3 reductions are - // required because c is 33 bits and so the first round will reduce from 512 - // bits to a max of 256 + 33 = 289 bits and the second round will reduce to - // within 2p. Then, a conditional subtraction of p handles the final - // reduction. - - var t0, t1, t2, t3, t4, h, lo, hi, carry uint64 - - h, t0 = bits.Mul64(x[4], field64PrimeComplement) - - // Note that since hi is the upper 64 bits of the product of a uint64 with - // c and c < 2^33: - // hi ≤ floor((2^64-1)(2^33 - 1) / 2^64) = 2^33 - 2 - // - // Then, because carry ≤ 1, a loose bound for h is: - // h ≤ hi + 1 = 2^33 - 1 < 2^64 - // - // Therefore, it is safe to discard the carry and the same applies to the - // next two limbs (second h and first t4). - hi, lo = bits.Mul64(x[5], field64PrimeComplement) - t1, carry = bits.Add64(lo, h, 0) - h, _ = bits.Add64(hi, 0, carry) - - hi, lo = bits.Mul64(x[6], field64PrimeComplement) - t2, carry = bits.Add64(lo, h, 0) - h, _ = bits.Add64(hi, 0, carry) - - hi, lo = bits.Mul64(x[7], field64PrimeComplement) - t3, carry = bits.Add64(lo, h, 0) - t4, _ = bits.Add64(hi, 0, carry) - - // The carryless add into t4 below is safe because, per the bound above, - // t4 ≤ 2^33 - 1 and carry ≤ 1, so: - // t4 ≤ (2^33 - 1) + 1 = 2^33 < 2^64 - t0, carry = bits.Add64(t0, x[0], 0) - t1, carry = bits.Add64(t1, x[1], carry) - t2, carry = bits.Add64(t2, x[2], carry) - t3, carry = bits.Add64(t3, x[3], carry) - t4 += carry - - // The value now fits in 289 bits, so reduce it again. Only the fifth limb - // (t4) needs to be considered since all of the higher limbs are ≥ 320 bits - // and thus guaranteed to be 0. - h, t4 = bits.Mul64(t4, field64PrimeComplement) - - t0, carry = bits.Add64(t0, t4, 0) - t1, carry = bits.Add64(t1, h, carry) - t2, carry = bits.Add64(t2, 0, carry) - t3, carry = bits.Add64(t3, 0, carry) - - // The second fold can carry out of t3. Keep it as a fifth limb (t4) and - // let the conditional subtract resolve it: the value is < 2p, so one 5-limb - // subtract of p fully reduces it. - t4 = carry - - var s0, s1, s2, s3, borrow uint64 - s0, borrow = bits.Sub64(t0, field64Prime0, 0) - s1, borrow = bits.Sub64(t1, field64Prime1, borrow) - s2, borrow = bits.Sub64(t2, field64Prime2, borrow) - s3, borrow = bits.Sub64(t3, field64Prime3, borrow) - _, borrow = bits.Sub64(t4, 0, borrow) - r[0] = arith.ConstantTimeSelect64(borrow, t0, s0) - r[1] = arith.ConstantTimeSelect64(borrow, t1, s1) - r[2] = arith.ConstantTimeSelect64(borrow, t2, s2) - r[3] = arith.ConstantTimeSelect64(borrow, t3, s3) -} - // Inverse finds the modular multiplicative inverse of the field value in // constant time. The existing field value is modified. // diff --git a/dcrec/secp256k1/field64_amd64.go b/dcrec/secp256k1/field64_amd64.go index e913c1fbd8..1774419d9b 100644 --- a/dcrec/secp256k1/field64_amd64.go +++ b/dcrec/secp256k1/field64_amd64.go @@ -2,12 +2,11 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -//go:build amd64 && !purego +//go:build !purego package secp256k1 import ( - "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/arith" "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/cpufeat" ) @@ -23,35 +22,3 @@ func field64MulReduceADX(r *[4]uint64, a, b *[4]uint64) //go:noescape func field64SquareReduceADX(r *[4]uint64, a *[4]uint64) - -// field64MulReduce sets r = a * b (mod p) -func field64MulReduce(r *[4]uint64, a, b *[4]uint64) { - if field64UseBMI2AndADX { - field64MulReduceADX(r, a, b) - return - } - field64MulReduceGeneric(r, a, b) -} - -// field64SquareReduce sets r = a^2 (mod p) -func field64SquareReduce(r *[4]uint64, a *[4]uint64) { - if field64UseBMI2AndADX { - field64SquareReduceADX(r, a) - return - } - field64SquareReduceGeneric(r, a) -} - -// field64MulReduceGeneric sets r = a * b (mod p) -func field64MulReduceGeneric(r *[4]uint64, a, b *[4]uint64) { - var product [8]uint64 - arith.Mul512(&product, a, b) - field64Reduce512(r, &product) -} - -// field64SquareReduceGeneric sets r = a^2 (mod p) -func field64SquareReduceGeneric(r *[4]uint64, a *[4]uint64) { - var product [8]uint64 - arith.Square512(&product, a) - field64Reduce512(r, &product) -} diff --git a/dcrec/secp256k1/field64_generic.go b/dcrec/secp256k1/field64_generic.go index 15ee98add8..3aa751235e 100644 --- a/dcrec/secp256k1/field64_generic.go +++ b/dcrec/secp256k1/field64_generic.go @@ -2,21 +2,122 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -//go:build !amd64 || purego - package secp256k1 -import "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/arith" +import ( + "math/bits" + + "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/arith" +) + +// field64Reduce512 reduces a 512-bit little-endian limb array modulo p in +// constant time and stores the result in r using pure Go. +func field64Reduce512(r *[4]uint64, x *[8]uint64) { + // This algorithm has been formally verified, including its intermediate + // bounds, carry assumptions, and functional correctness. The verification + // artifacts are available in internal/proofs. + + // Per [HAC] section 14.3.4: Reduction method of moduli of special form, + // when the modulus is of the special form m = b^t - c, highly efficient + // reduction can be achieved. While [HAC] only presents the algorithm and + // does not call it out by name or provide the mathematical justification, + // the underlying technique is known as Crandall reduction and is often + // presented as 2^k - c. It is easy to see they are equivalent by setting + // b = 2 and t = k. + // + // The secp256k1 prime is 2^256 - 4294968273, so it fits this criteria where + // k=256, and c = 4294968273 = 2^32 + 977. + // + // Crandall reduction works by taking advantage of the fact that if a prime + // is of the form 2^k - c, then 2^k - c ≡ 0 (mod p), so 2^k ≡ c (mod p). In + // other words, every multiple of 2^k is equivalent to adding c when working + // modulo p. + // + // Since the 512-bit value to reduce is tightly packed into uint64s, the + // upper 4 limbs are all multiples of 2^256. Therefore, reducing modulo the + // prime is equivalent to multiplying those upper limbs by c and adding the + // result to the corresponding lower 4 limbs while propagating the carries. + // + // For the specific case of the secp256k1 prime, a max of 3 reductions are + // required because c is 33 bits and so the first round will reduce from 512 + // bits to a max of 256 + 33 = 289 bits and the second round will reduce to + // within 2p. Then, a conditional subtraction of p handles the final + // reduction. + + var t0, t1, t2, t3, t4, h, lo, hi, carry uint64 + + h, t0 = bits.Mul64(x[4], field64PrimeComplement) + + // Note that since hi is the upper 64 bits of the product of a uint64 with + // c and c < 2^33: + // hi ≤ floor((2^64-1)(2^33 - 1) / 2^64) = 2^33 - 2 + // + // Then, because carry ≤ 1, a loose bound for h is: + // h ≤ hi + 1 = 2^33 - 1 < 2^64 + // + // Therefore, it is safe to discard the carry and the same applies to the + // next two limbs (second h and first t4). + hi, lo = bits.Mul64(x[5], field64PrimeComplement) + t1, carry = bits.Add64(lo, h, 0) + h, _ = bits.Add64(hi, 0, carry) + + hi, lo = bits.Mul64(x[6], field64PrimeComplement) + t2, carry = bits.Add64(lo, h, 0) + h, _ = bits.Add64(hi, 0, carry) + + hi, lo = bits.Mul64(x[7], field64PrimeComplement) + t3, carry = bits.Add64(lo, h, 0) + t4, _ = bits.Add64(hi, 0, carry) + + // The carryless add into t4 below is safe because, per the bound above, + // t4 ≤ 2^33 - 1 and carry ≤ 1, so: + // t4 ≤ (2^33 - 1) + 1 = 2^33 < 2^64 + t0, carry = bits.Add64(t0, x[0], 0) + t1, carry = bits.Add64(t1, x[1], carry) + t2, carry = bits.Add64(t2, x[2], carry) + t3, carry = bits.Add64(t3, x[3], carry) + t4 += carry + + // The value now fits in 289 bits, so reduce it again. Only the fifth limb + // (t4) needs to be considered since all of the higher limbs are ≥ 320 bits + // and thus guaranteed to be 0. + h, t4 = bits.Mul64(t4, field64PrimeComplement) + + t0, carry = bits.Add64(t0, t4, 0) + t1, carry = bits.Add64(t1, h, carry) + t2, carry = bits.Add64(t2, 0, carry) + t3, carry = bits.Add64(t3, 0, carry) + + // The second fold can carry out of t3. Keep it as a fifth limb (t4) and + // let the conditional subtract resolve it: the value is < 2p, so one 5-limb + // subtract of p fully reduces it. + t4 = carry + + var s0, s1, s2, s3, borrow uint64 + s0, borrow = bits.Sub64(t0, field64Prime0, 0) + s1, borrow = bits.Sub64(t1, field64Prime1, borrow) + s2, borrow = bits.Sub64(t2, field64Prime2, borrow) + s3, borrow = bits.Sub64(t3, field64Prime3, borrow) + _, borrow = bits.Sub64(t4, 0, borrow) + r[0] = arith.ConstantTimeSelect64(borrow, t0, s0) + r[1] = arith.ConstantTimeSelect64(borrow, t1, s1) + r[2] = arith.ConstantTimeSelect64(borrow, t2, s2) + r[3] = arith.ConstantTimeSelect64(borrow, t3, s3) +} -// field64MulReduce sets r = a * b (mod p). -func field64MulReduce(r *[4]uint64, a, b *[4]uint64) { +// field64MulReduceGeneric sets r = a * b (mod p). This is a generic +// implementation that performs the multiplication and reduction steps +// separately without any reliance on specific hardware extensions. +func field64MulReduceGeneric(r *[4]uint64, a, b *[4]uint64) { var product [8]uint64 arith.Mul512(&product, a, b) field64Reduce512(r, &product) } -// field64SquareReduce sets r = a^2 (mod p). -func field64SquareReduce(r *[4]uint64, a *[4]uint64) { +// field64SquareReduceGeneric sets r = a^2 (mod p). This is a generic +// implementation that performs the squaring and reduction steps separately +// without any reliance on specific hardware extensions. +func field64SquareReduceGeneric(r *[4]uint64, a *[4]uint64) { var product [8]uint64 arith.Square512(&product, a) field64Reduce512(r, &product) diff --git a/dcrec/secp256k1/field64_noasm.go b/dcrec/secp256k1/field64_noasm.go new file mode 100644 index 0000000000..d6767fd3c2 --- /dev/null +++ b/dcrec/secp256k1/field64_noasm.go @@ -0,0 +1,19 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +//go:build !amd64 || purego + +package secp256k1 + +// field64MulReduce sets r = a * b (mod p). This defers to the generic +// implementation that does not rely on the optimized assembly implementations. +func field64MulReduce(r *[4]uint64, a, b *[4]uint64) { + field64MulReduceGeneric(r, a, b) +} + +// field64SquareReduce sets r = a^2 (mod p). This defers to the generic +// implementation that does not rely on the optimized assembly implementations. +func field64SquareReduce(r *[4]uint64, a *[4]uint64) { + field64SquareReduceGeneric(r, a) +} diff --git a/dcrec/secp256k1/field64isa_amd64.go b/dcrec/secp256k1/field64isa_amd64.go new file mode 100644 index 0000000000..8f93dbefed --- /dev/null +++ b/dcrec/secp256k1/field64isa_amd64.go @@ -0,0 +1,27 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +//go:build !purego + +package secp256k1 + +// field64MulReduce sets r = a * b (mod p) using processor-specific hardware +// extensions when available. +func field64MulReduce(r *[4]uint64, a, b *[4]uint64) { + if field64UseBMI2AndADX { + field64MulReduceADX(r, a, b) + return + } + field64MulReduceGeneric(r, a, b) +} + +// field64SquareReduce sets r = a^2 (mod p) using processor-specific hardware +// extensions when available. +func field64SquareReduce(r *[4]uint64, a *[4]uint64) { + if field64UseBMI2AndADX { + field64SquareReduceADX(r, a) + return + } + field64SquareReduceGeneric(r, a) +} From 1ed11768b19a36e7b21b098f832765df448a9553 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 30 Jul 2026 04:02:40 -0500 Subject: [PATCH 25/33] secp256k1: Add amd64 mul/square variant tests. This adds tests to ensure all supported variants of the mul and square methods that choose different implementations at run time depending on available hardware are exercised when run on hardware that supports them. In particular, because pure Go is always supported, it ensures the pure Go code paths are for the methods are always tested versus only testing the optimized paths. --- dcrec/secp256k1/field64_amd64_test.go | 66 +++++++++++++++++++++++++++ dcrec/secp256k1/field64_noasm.go | 4 ++ dcrec/secp256k1/field64_test.go | 30 +++++++++--- 3 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 dcrec/secp256k1/field64_amd64_test.go diff --git a/dcrec/secp256k1/field64_amd64_test.go b/dcrec/secp256k1/field64_amd64_test.go new file mode 100644 index 0000000000..90f3c4f2fc --- /dev/null +++ b/dcrec/secp256k1/field64_amd64_test.go @@ -0,0 +1,66 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package secp256k1 + +import "testing" + +// TestField64AMD64 ensures each of the specialized amd64 [FieldVal64.Mul] and +// [FieldVal64.Square] implementations return the expected results. +// +// Note that any tests which require instruction sets that aren't available on +// the system executing the tests are skipped. +func TestField64AMD64(t *testing.T) { + // NOTE: This is intentionally not made parallel because it modifies the + // global feature flags to ensure all supported variants are tested. + + pureGo := true + type mulVariantTest struct { + name string + featureFlag *bool + } + variants := []mulVariantTest{ + {name: "Pure Go", featureFlag: &pureGo}, + {name: "BMI2/ADX", featureFlag: &field64UseBMI2AndADX}, + } + + // Restore the feature flags after the tests complete. + origFlags := make(map[string]bool) + for _, variant := range variants { + origFlags[variant.name] = *variant.featureFlag + } + defer func() { + for _, variant := range variants { + *variant.featureFlag = origFlags[variant.name] + } + }() + + for _, variant := range variants { + t.Run(variant.name, func(t *testing.T) { + // Skip any features that the hardware does not support or have + // explicitly been disabled. + // + // Note that this is intentionally not using t.Skipf because tinygo + // does not support it. + if !origFlags[variant.name] { + t.Logf("Skipping %s tests (disabled or no instruction set "+ + "support)", variant.name) + return + } + + // Ensure only the specific feature flag for this test is enabled. + for _, variant := range variants { + *variant.featureFlag = false + } + *variant.featureFlag = true + + t.Run("Mul", func(t *testing.T) { + testField64Mul(t) + }) + t.Run("Square", func(t *testing.T) { + testField64Square(t) + }) + }) + } +} diff --git a/dcrec/secp256k1/field64_noasm.go b/dcrec/secp256k1/field64_noasm.go index d6767fd3c2..840a84826a 100644 --- a/dcrec/secp256k1/field64_noasm.go +++ b/dcrec/secp256k1/field64_noasm.go @@ -6,6 +6,10 @@ package secp256k1 +// field64UseBMI2AndADX depends on hardware support and access to assembly +// instructions. +var field64UseBMI2AndADX = false + // field64MulReduce sets r = a * b (mod p). This defers to the generic // implementation that does not rely on the optimized assembly implementations. func field64MulReduce(r *[4]uint64, a, b *[4]uint64) { diff --git a/dcrec/secp256k1/field64_test.go b/dcrec/secp256k1/field64_test.go index a3589130ac..c2ff02d0f3 100644 --- a/dcrec/secp256k1/field64_test.go +++ b/dcrec/secp256k1/field64_test.go @@ -906,9 +906,12 @@ func TestField64MulInt(t *testing.T) { } } -// TestField64Mul ensures that multiplying two field values via Mul and Mul2 -// works as expected. -func TestField64Mul(t *testing.T) { +// testField64Mul ensures that multiplying two field values via [Field64Val.Mul] +// and [Field64Val.Mul2] works as expected. This unexported version exists so +// it can be invoked with different run-time achitecture variants. +func testField64Mul(t *testing.T) { + t.Helper() + tests := []struct { name string // test description in1 string // first hex encoded value @@ -1006,9 +1009,18 @@ func TestField64Mul(t *testing.T) { } } -// TestField64Square ensures that squaring field values via Square and SquareVal -// works as expected. -func TestField64Square(t *testing.T) { +// TestField64Mul ensures that multiplying two field values via [Field64Val.Mul] +// and [Field64Val.Mul2] works as expected. +func TestField64Mul(t *testing.T) { + testField64Mul(t) +} + +// testField64Square ensures that squaring field values via [Field64Val.Square] +// and [Field64.SquareVal] works as expected. This unexported version exists so +// it can be invoked with different run-time achitecture variants. +func testField64Square(t *testing.T) { + t.Helper() + tests := []struct { name string // test description in string // hex encoded value @@ -1071,6 +1083,12 @@ func TestField64Square(t *testing.T) { } } +// TestField64Square ensures that squaring field values via [Field64Val.Square] +// and [Field64.SquareVal] works as expected. +func TestField64Square(t *testing.T) { + testField64Square(t) +} + // TestField64SquareRoot ensures that calculating the square root of field values // via SquareRootVal works as expected for edge cases. func TestField64SquareRoot(t *testing.T) { From 449ce22656c3c638b5f4096505fee778bb3ac49d Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 29 Jul 2026 01:09:44 -0500 Subject: [PATCH 26/33] secp256k1: Move 10x26 field code to subpackage. Historically, there has only ever been a single field implementation and the current release of the module reflects that fact with a single exported FieldVal for that implementation. However, recent work has introduced a new FieldVal64 implementation which is significantly more efficient on 64-bit hardware. Ultimately, the main package will need to select the implementation based on the target architecture, but having two different types with different semantics exposed from the main package would be confusing. At the same time, consumers should retain the flexibility to use a specific implementation directly if their particular use case warrants it. The primary design goals are: - Avoid breaking the public API in a way that would require a major module version bump - Make both variants available to consumers irrespective of the choice the module makes for internally performing its own calculations over the curve - This allows them to make independent calculations using their preferred implementation - Ensure the public FieldVal documentation always reflects the most restrictive semantics among all supported field implementations - Note that a simple type alias that changes based on the architecture does not provide this property - Provide a clean structure that allows adding additional assembler optimizations without bloating the main package After considering a variety of approaches, the following design satisfies all of those goals: - Split the field implementations into separate subpackages and rename them to Element within their respective packages - Make FieldVal a thin wrapper over the chosen implementation The required changes are largely mechanical, but they are quite extensive. So, with the goal of making it much easier to review and reason about the correctness of the changes, this is the first in a series of commits that will ultimately land on the described design. This first commit moves the entire FieldVal implementation, tests, and benchmarks into a separate package named field10x26 and only makes the minimal required changes to ensure the code compiles and all tests pass. It also temporarily aliases FieldVal to the new package. That alias will be replaced with the wrapper in a subsequent commit. No functional changes are intended by this commit. It is strictly a refactoring that preserves the existing behavior while preparing for the architecture-specific backend selection. --- dcrec/secp256k1/curve.go | 4 + dcrec/secp256k1/curve_test.go | 28 ++++++ dcrec/secp256k1/field10x26/common_test.go | 98 +++++++++++++++++++ .../{field.go => field10x26/element.go} | 7 +- .../element_bench_test.go} | 2 +- .../element_test.go} | 30 +----- 6 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 dcrec/secp256k1/field10x26/common_test.go rename dcrec/secp256k1/{field.go => field10x26/element.go} (99%) rename dcrec/secp256k1/{field_bench_test.go => field10x26/element_bench_test.go} (99%) rename dcrec/secp256k1/{field_test.go => field10x26/element_test.go} (98%) diff --git a/dcrec/secp256k1/curve.go b/dcrec/secp256k1/curve.go index fb9da3d265..ee440a8402 100644 --- a/dcrec/secp256k1/curve.go +++ b/dcrec/secp256k1/curve.go @@ -8,6 +8,8 @@ package secp256k1 import ( "encoding/hex" "math/bits" + + "github.com/decred/dcrd/dcrec/secp256k1/v4/field10x26" ) // References: @@ -26,6 +28,8 @@ import ( // (x, y) position on the curve, the Jacobian coordinates are (x1, y1, z1) // where x = x1/z1^2 and y = y1/z1^3. +type FieldVal = field10x26.FieldVal + // mustFieldValInternal converts the passed hex string into a [FieldVal] and // will panic if there is an error. Values that overflow are treated as an // error unless the allow overflow flag is set. diff --git a/dcrec/secp256k1/curve_test.go b/dcrec/secp256k1/curve_test.go index 3f32a39a4e..6ddb60fdbb 100644 --- a/dcrec/secp256k1/curve_test.go +++ b/dcrec/secp256k1/curve_test.go @@ -61,6 +61,34 @@ func (p *JacobianPoint) Rescale(s *FieldVal) { p.Z.Mul(s).Normalize() } +// mustFieldValWithOverflow converts the passed hex string into a [FieldVal] and +// will panic if there is an error. Values that overflow are NOT treated as an +// error. +// +// This is only provided for the hard-coded constants so errors in the source +// code can be detected. It will only (and must only) be called with hard-coded +// values. +func mustFieldValWithOverflow(s string) *FieldVal { + return mustFieldValInternal(s, true) +} + +// randFieldVal returns a field value created from a random value generated by +// the passed rng. +func randFieldVal(t *testing.T, rng *rand.Rand) *FieldVal { + t.Helper() + + var buf [32]byte + if _, err := rng.Read(buf[:]); err != nil { + t.Fatalf("failed to read random: %v", err) + } + + // Create and return a field value. + var fv FieldVal + fv.SetBytes(&buf) + fv.Normalize() + return &fv +} + // randJacobian returns a Jacobian point created from a point generated by the // passed rng. func randJacobian(t *testing.T, rng *rand.Rand) *JacobianPoint { diff --git a/dcrec/secp256k1/field10x26/common_test.go b/dcrec/secp256k1/field10x26/common_test.go new file mode 100644 index 0000000000..3f1ae20ac9 --- /dev/null +++ b/dcrec/secp256k1/field10x26/common_test.go @@ -0,0 +1,98 @@ +// Copyright (c) 2026 The Decred developers +// Copyright (c) 2013-2026 Dave Collins +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package field10x26 + +// References: +// [SECG]: Recommended Elliptic Curve Domain Parameters +// https://www.secg.org/sec2-v2.pdf +// + +import ( + "encoding/hex" + "math/big" +) + +// Curve parameters taken from [SECG] section 2.4.1. +var curveParams = struct { + P *big.Int + N *big.Int +}{ + P: fromHex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"), + N: fromHex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"), +} + +// hexToBytes converts the passed hex string into bytes and will panic if there +// is an error. This is only provided for the hard-coded constants so errors in +// the source code can be detected. It will only (and must only) be called with +// hard-coded values. +func hexToBytes(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic("invalid hex in source file: " + s) + } + return b +} + +// fromHex converts the passed hex string into a big integer pointer and will +// panic is there is an error. This is only provided for the hard-coded +// constants so errors in the source code can bet detected. It will only (and +// must only) be called for initialization purposes. +func fromHex(s string) *big.Int { + if s == "" { + return big.NewInt(0) + } + r, ok := new(big.Int).SetString(s, 16) + if !ok { + panic("invalid hex in source file: " + s) + } + return r +} + +// mustFieldValInternal converts the passed hex string into a [FieldVal] and +// will panic if there is an error. Values that overflow are treated as an +// error unless the allow overflow flag is set. +// +// This is only provided for the hard-coded constants so errors in the source +// code can be detected. It will only (and must only) be called with hard-coded +// values. +func mustFieldValInternal(s string, allowOverflow bool) *FieldVal { + if len(s)%2 != 0 { + s = "0" + s + } + b, err := hex.DecodeString(s) + if err != nil { + panic("invalid hex in source file: " + s) + } + if len(b) > 32 { + panic("hex in source file overflows uint256: " + s) + } + var f FieldVal + if overflow := f.SetByteSlice(b); overflow && !allowOverflow { + panic("hex in source file overflows mod N scalar: " + s) + } + return &f +} + +// mustFieldVal converts the passed hex string into a [FieldVal] and will panic +// if there is an error. Values that overflow are treated as an error. +// +// This is only provided for the hard-coded constants so errors in the source +// code can be detected. It will only (and must only) be called with hard-coded +// values. +func mustFieldVal(s string) *FieldVal { + return mustFieldValInternal(s, false) +} + +// mustFieldValWithOverflow converts the passed hex string into a [FieldVal] and +// will panic if there is an error. Values that overflow are NOT treated as an +// error. +// +// This is only provided for the hard-coded constants so errors in the source +// code can be detected. It will only (and must only) be called with hard-coded +// values. +func mustFieldValWithOverflow(s string) *FieldVal { + return mustFieldValInternal(s, true) +} diff --git a/dcrec/secp256k1/field.go b/dcrec/secp256k1/field10x26/element.go similarity index 99% rename from dcrec/secp256k1/field.go rename to dcrec/secp256k1/field10x26/element.go index 76a9c48742..f934169919 100644 --- a/dcrec/secp256k1/field.go +++ b/dcrec/secp256k1/field10x26/element.go @@ -4,7 +4,7 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package secp256k1 +package field10x26 import ( "encoding/hex" @@ -300,6 +300,11 @@ func (f *FieldVal) SetBytes(b *[32]byte) uint32 { return overflow } +// zeroArray32 zeroes the provided 32-byte buffer. +func zeroArray32(b *[32]byte) { + *b = [32]byte{} +} + // SetByteSlice interprets the provided slice as a 256-bit big-endian unsigned // integer (meaning it is truncated to the first 32 bytes), packs it into the // internal field value representation, and returns whether or not the resulting diff --git a/dcrec/secp256k1/field_bench_test.go b/dcrec/secp256k1/field10x26/element_bench_test.go similarity index 99% rename from dcrec/secp256k1/field_bench_test.go rename to dcrec/secp256k1/field10x26/element_bench_test.go index bdf2c16b12..11c34b106f 100644 --- a/dcrec/secp256k1/field_bench_test.go +++ b/dcrec/secp256k1/field10x26/element_bench_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package secp256k1 +package field10x26 import ( "math/big" diff --git a/dcrec/secp256k1/field_test.go b/dcrec/secp256k1/field10x26/element_test.go similarity index 98% rename from dcrec/secp256k1/field_test.go rename to dcrec/secp256k1/field10x26/element_test.go index 41a5a61757..a38fe0df7a 100644 --- a/dcrec/secp256k1/field_test.go +++ b/dcrec/secp256k1/field10x26/element_test.go @@ -4,7 +4,7 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package secp256k1 +package field10x26 import ( "bytes" @@ -16,34 +16,6 @@ import ( "time" ) -// mustFieldValWithOverflow converts the passed hex string into a [FieldVal] and -// will panic if there is an error. Values that overflow are NOT treated as an -// error. -// -// This is only provided for the hard-coded constants so errors in the source -// code can be detected. It will only (and must only) be called with hard-coded -// values. -func mustFieldValWithOverflow(s string) *FieldVal { - return mustFieldValInternal(s, true) -} - -// randFieldVal returns a field value created from a random value generated by -// the passed rng. -func randFieldVal(t *testing.T, rng *rand.Rand) *FieldVal { - t.Helper() - - var buf [32]byte - if _, err := rng.Read(buf[:]); err != nil { - t.Fatalf("failed to read random: %v", err) - } - - // Create and return a field value. - var fv FieldVal - fv.SetBytes(&buf) - fv.Normalize() - return &fv -} - // randIntAndFieldVal returns a big integer and a field value both created from // the same random value generated by the passed rng. func randIntAndFieldVal(t *testing.T, rng *rand.Rand) (*big.Int, *FieldVal) { From e4097065e22e89898c945dd6d661a857e138a334 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 29 Jul 2026 01:09:45 -0500 Subject: [PATCH 27/33] secp256k1: Rename field10x26.FieldVal to Element. This renames FieldVal in the new field10x26 package to Element to more accurately reflect that it is an implementation-specific field element rather than the public compatibility type. It also renames all tests and benchmarks to match and updates various comments accordingly. While here, also update the internal terminology to consistently use the more technically accurate term "limbs". It is a large change in terms of lines changed, but it is entirely mechanical. No functional changes are intended by this commit. It is strictly a refactoring that preserves the existing behavior. This is part of a series of commits that will ultimately allow architecture-specific backend selection. --- dcrec/secp256k1/curve.go | 2 +- dcrec/secp256k1/field10x26/common_test.go | 26 +- dcrec/secp256k1/field10x26/element.go | 1219 ++++++++--------- .../field10x26/element_bench_test.go | 150 +- dcrec/secp256k1/field10x26/element_test.go | 397 +++--- 5 files changed, 897 insertions(+), 897 deletions(-) diff --git a/dcrec/secp256k1/curve.go b/dcrec/secp256k1/curve.go index ee440a8402..e6c6d4f629 100644 --- a/dcrec/secp256k1/curve.go +++ b/dcrec/secp256k1/curve.go @@ -28,7 +28,7 @@ import ( // (x, y) position on the curve, the Jacobian coordinates are (x1, y1, z1) // where x = x1/z1^2 and y = y1/z1^3. -type FieldVal = field10x26.FieldVal +type FieldVal = field10x26.Element // mustFieldValInternal converts the passed hex string into a [FieldVal] and // will panic if there is an error. Values that overflow are treated as an diff --git a/dcrec/secp256k1/field10x26/common_test.go b/dcrec/secp256k1/field10x26/common_test.go index 3f1ae20ac9..d474680725 100644 --- a/dcrec/secp256k1/field10x26/common_test.go +++ b/dcrec/secp256k1/field10x26/common_test.go @@ -51,14 +51,14 @@ func fromHex(s string) *big.Int { return r } -// mustFieldValInternal converts the passed hex string into a [FieldVal] and -// will panic if there is an error. Values that overflow are treated as an -// error unless the allow overflow flag is set. +// mustElementInternal converts the passed hex string into an [Element] and will +// panic if there is an error. Values that overflow are treated as an error +// unless the allow overflow flag is set. // // This is only provided for the hard-coded constants so errors in the source // code can be detected. It will only (and must only) be called with hard-coded // values. -func mustFieldValInternal(s string, allowOverflow bool) *FieldVal { +func mustElementInternal(s string, allowOverflow bool) *Element { if len(s)%2 != 0 { s = "0" + s } @@ -69,30 +69,30 @@ func mustFieldValInternal(s string, allowOverflow bool) *FieldVal { if len(b) > 32 { panic("hex in source file overflows uint256: " + s) } - var f FieldVal - if overflow := f.SetByteSlice(b); overflow && !allowOverflow { + var e Element + if overflow := e.SetByteSlice(b); overflow && !allowOverflow { panic("hex in source file overflows mod N scalar: " + s) } - return &f + return &e } -// mustFieldVal converts the passed hex string into a [FieldVal] and will panic +// mustElement converts the passed hex string into an [Element] and will panic // if there is an error. Values that overflow are treated as an error. // // This is only provided for the hard-coded constants so errors in the source // code can be detected. It will only (and must only) be called with hard-coded // values. -func mustFieldVal(s string) *FieldVal { - return mustFieldValInternal(s, false) +func mustElement(s string) *Element { + return mustElementInternal(s, false) } -// mustFieldValWithOverflow converts the passed hex string into a [FieldVal] and +// mustElementWithOverflow converts the passed hex string into an [Element] and // will panic if there is an error. Values that overflow are NOT treated as an // error. // // This is only provided for the hard-coded constants so errors in the source // code can be detected. It will only (and must only) be called with hard-coded // values. -func mustFieldValWithOverflow(s string) *FieldVal { - return mustFieldValInternal(s, true) +func mustElementWithOverflow(s string) *Element { + return mustElementInternal(s, true) } diff --git a/dcrec/secp256k1/field10x26/element.go b/dcrec/secp256k1/field10x26/element.go index f934169919..1d40e5da4a 100644 --- a/dcrec/secp256k1/field10x26/element.go +++ b/dcrec/secp256k1/field10x26/element.go @@ -4,6 +4,9 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. +// Package field10x26 implements highly optimized, constant-time arithmetic +// over the secp256k1 finite field using a 10x26 representation with bounded +// overflow between normalizations. package field10x26 import ( @@ -26,9 +29,9 @@ import ( // optimizations not available to arbitrary-precision arithmetic and generic // modular arithmetic algorithms. // -// There are various ways to internally represent each finite field element. -// For example, the most obvious representation would be to use an array of 4 -// uint64s (aka 4x64: 64 bits * 4 = 256 bits). However, at the time this field +// There are various internal representations for finite field elements. For +// example, the most obvious representation would be to use an array of 4 +// uint64s (aka 4x64: 64 bits * 4 = 256 bits). However, at the time this // implementation was written, Go did not have access to hardware intrinsics, so // that representation suffered from a couple of issues. First, there is no // native Go type large enough to handle the intermediate results while adding @@ -39,7 +42,7 @@ import ( // While both of those things are still true without intrinsics, modern Go now // provides access to intrinsics that permit the hardware to perform both full // 128-bit products and addition with carry which entirely mitigates those -// limitations. As a result, there is now an alternative [FieldVal64] +// limitations. As a result, there is now an alternative [secp256k1.FieldVal64] // implementation that uses the aforementioned 4x64 representation with // intrinsics. // @@ -63,7 +66,7 @@ import ( // // Since it is so important that the field arithmetic is extremely fast for high // performance crypto, this type does not perform any validation where it -// ordinarily would. See the documentation for [FieldVal] for more details. +// ordinarily would. See the documentation for [Element] for more details. // Constants used to make the code more readable. const ( @@ -73,48 +76,48 @@ const ( eightBitsMask = 0xff ) -// Constants related to the field representation. +// Constants related to the internal representation. const ( - // fieldWords is the number of words used to internally represent the + // fieldLimbs is the number of limbs used to internally represent the // 256-bit value. - fieldWords = 10 + fieldLimbs = 10 - // fieldBase is the exponent used to form the numeric base of each word. - // 2^(fieldBase*i) where i is the word position. + // fieldBase is the exponent used to form the numeric base of each limb. + // 2^(fieldBase*i) where i is the limb position. fieldBase = 26 - // fieldBaseMask is the mask for the bits in each word needed to - // represent the numeric base of each word (except the most significant - // word). + // fieldBaseMask is the mask for the bits in each limb needed to + // represent the numeric base of each limb (except the most significant + // limb). fieldBaseMask = (1 << fieldBase) - 1 - // fieldMSBBits is the number of bits in the most significant word used + // fieldMSBBits is the number of bits in the most significant limb used // to represent the value. - fieldMSBBits = 256 - (fieldBase * (fieldWords - 1)) + fieldMSBBits = 256 - (fieldBase * (fieldLimbs - 1)) - // fieldMSBMask is the mask for the bits in the most significant word + // fieldMSBMask is the mask for the bits in the most significant limb // needed to represent the value. fieldMSBMask = (1 << fieldMSBBits) - 1 - // These fields provide convenient access to each of the words of the - // secp256k1 prime in the internal field representation to improve code + // These fields provide convenient access to each of the limbs of the + // secp256k1 prime in the internal representation to improve code // readability. - fieldPrimeWordZero = 0x03fffc2f - fieldPrimeWordOne = 0x03ffffbf - fieldPrimeWordTwo = 0x03ffffff - fieldPrimeWordThree = 0x03ffffff - fieldPrimeWordFour = 0x03ffffff - fieldPrimeWordFive = 0x03ffffff - fieldPrimeWordSix = 0x03ffffff - fieldPrimeWordSeven = 0x03ffffff - fieldPrimeWordEight = 0x03ffffff - fieldPrimeWordNine = 0x003fffff + fieldPrimeLimb0 = 0x03fffc2f + fieldPrimeLimb1 = 0x03ffffbf + fieldPrimeLimb2 = 0x03ffffff + fieldPrimeLimb3 = 0x03ffffff + fieldPrimeLimb4 = 0x03ffffff + fieldPrimeLimb5 = 0x03ffffff + fieldPrimeLimb6 = 0x03ffffff + fieldPrimeLimb7 = 0x03ffffff + fieldPrimeLimb8 = 0x03ffffff + fieldPrimeLimb9 = 0x003fffff ) -// FieldVal implements optimized fixed-precision arithmetic over the -// secp256k1 finite field. This means all arithmetic is performed modulo +// Element implements optimized fixed-precision arithmetic over the secp256k1 +// finite field. This means all arithmetic is performed modulo // -// 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f. +// 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f // // WARNING: Since it is so important for the field arithmetic to be extremely // fast for high performance crypto, this type does not perform any validation @@ -122,38 +125,38 @@ const ( // IMPERATIVE for callers to understand some key concepts that are described // below and ensure the methods are called with the necessary preconditions that // each method is documented with. For example, some methods only give the -// correct result if the field value is normalized and others require the field -// values involved to have a maximum magnitude and THERE ARE NO EXPLICIT CHECKS -// TO ENSURE THOSE PRECONDITIONS ARE SATISFIED. This does, unfortunately, make -// the type more difficult to use correctly and while I typically prefer to +// correct result if the element is normalized and others require the elements +// involved to have a maximum magnitude and THERE ARE NO EXPLICIT CHECKS TO +// ENSURE THOSE PRECONDITIONS ARE SATISFIED. This does, unfortunately, make the +// type more difficult to use correctly and while it is typically preferable to // ensure all state and input is valid for most code, this is a bit of an // exception because those extra checks really add up in what ends up being // critical hot paths. // // The first key concept when working with this type is normalization. In order // to avoid the need to propagate a ton of carries, the internal representation -// provides additional overflow bits for each word of the overall 256-bit value. +// provides additional overflow bits for each limb of the overall 256-bit value. // This means that there are multiple internal representations for the same // value and, as a result, any methods that rely on comparison of the value, // such as equality and oddness determination, require the caller to provide a -// normalized value. +// normalized field element. // // The second key concept when working with this type is magnitude. As // previously mentioned, the internal representation provides additional // overflow bits which means that the more math operations that are performed on -// the field value between normalizations, the more those overflow bits -// accumulate. The magnitude is effectively that maximum possible number of -// those overflow bits that could possibly be required as a result of a given -// operation. Since there are only a limited number of overflow bits available, -// this implies that the max possible magnitude MUST be tracked by the caller -// and the caller MUST normalize the field value if a given operation would -// cause the magnitude of the result to exceed the max allowed value. -// -// IMPORTANT: The max allowed magnitude of a field value is 32. -type FieldVal struct { +// the element between normalizations, the more those overflow bits accumulate. +// The magnitude is effectively that maximum possible number of those overflow +// bits that could possibly be required as a result of a given operation. Since +// there are only a limited number of overflow bits available, this implies that +// the max possible magnitude MUST be tracked by the caller and the caller MUST +// normalize the element if a given operation would cause the magnitude of the +// result to exceed the max allowed value. +// +// IMPORTANT: The max allowed magnitude of an element is 32. +type Element struct { // Each 256-bit value is represented as 10 32-bit integers in base 2^26. - // This provides 6 bits of overflow in each word (10 bits in the most - // significant word) for a total of 64 bits of overflow (9*6 + 10 = 64). It + // This provides 6 bits of overflow in each limb (10 bits in the most + // significant limb) for a total of 64 bits of overflow (9*6 + 10 = 64). It // only implements the arithmetic needed for elliptic curve operations. // // The following depicts the internal representation: @@ -181,121 +184,120 @@ type FieldVal struct { n [10]uint32 } -// String returns the field value as a normalized human-readable hex string. +// String returns the element as a normalized human-readable hex string. // // Preconditions: None -// Output Normalized: Field is not modified -- same as input value -// Output Max Magnitude: Field is not modified -- same as input value -func (f FieldVal) String() string { - // f is a copy, so it's safe to normalize it without mutating the original. - f.Normalize() - return hex.EncodeToString(f.Bytes()[:]) +// Output Normalized: Element is not modified -- same as input element +// Output Max Magnitude: Element is not modified -- same as input element +func (e Element) String() string { + // e is a copy, so it's safe to normalize it without mutating the original. + e.Normalize() + return hex.EncodeToString(e.Bytes()[:]) } -// Zero sets the field value to zero in constant time. A newly created field -// value is already set to zero. This function can be useful to clear an -// existing field value for reuse. +// Zero sets the element to zero in constant time. A newly created element is +// already set to zero. This function can be useful to clear an existing +// element for reuse. // // Preconditions: None // Output Normalized: Yes // Output Max Magnitude: 1 -func (f *FieldVal) Zero() { - f.n = [10]uint32{} +func (e *Element) Zero() { + e.n = [10]uint32{} } -// Set sets the field value equal to the passed value in constant time. The -// normalization and magnitude of the two fields will be identical. +// Set sets the element equal to the passed element in constant time. The +// normalization and magnitude of the two elements will be identical. // -// The field value is returned to support chaining. This enables syntax like: -// f := new(FieldVal).Set(f2).Add(1) so that f = f2 + 1 where f2 is not -// modified. +// The element is returned to support chaining. This enables syntax like: +// e := new(Element).Set(e2).Add(1) so that e = e2 + 1 where e2 is not modified. // // Preconditions: None -// Output Normalized: Same as input value -// Output Max Magnitude: Same as input value -func (f *FieldVal) Set(val *FieldVal) *FieldVal { - *f = *val - return f +// Output Normalized: Same as input element +// Output Max Magnitude: Same as input element +func (e *Element) Set(val *Element) *Element { + *e = *val + return e } -// SetInt sets the field value to the passed integer in constant time. This is -// a convenience function since it is fairly common to perform some arithmetic -// with small native integers. +// SetInt sets the element to the passed integer in constant time. This is a +// convenience function since it is fairly common to perform arithmetic with +// small native integers. // -// The field value is returned to support chaining. This enables syntax such -// as f := new(FieldVal).SetInt(2).Mul(f2) so that f = 2 * f2. +// The element is returned to support chaining. This enables syntax such +// as e := new(Element).SetInt(2).Mul(e2) so that e = 2 * e2. // // Preconditions: None // Output Normalized: Yes // Output Max Magnitude: 1 -func (f *FieldVal) SetInt(ui uint16) *FieldVal { - f.Zero() - f.n[0] = uint32(ui) - return f +func (e *Element) SetInt(ui uint16) *Element { + e.Zero() + e.n[0] = uint32(ui) + return e } -// SetBytes packs the passed 32-byte big-endian value into the internal field -// value representation in constant time. It interprets the provided array as a -// 256-bit big-endian unsigned integer, packs it into the internal field value -// representation, and returns either 1 if it is greater than or equal to the -// field prime (aka it overflowed) or 0 otherwise in constant time. +// SetBytes packs the passed 32-byte big-endian value into the internal +// representation in constant time. It interprets the provided array as a +// 256-bit big-endian unsigned integer, packs it, and returns either 1 if it is +// greater than or equal to the field prime (aka it overflowed) or 0 otherwise +// in constant time. // // Note that a bool is not used here because it is not possible in Go to convert // from a bool to numeric value in constant time and many constant-time // operations require a numeric value. // // Preconditions: None -// Output Normalized: Yes if no overflow, no otherwise +// Output Normalized: Yes when no overflow, No otherwise // Output Max Magnitude: 1 -func (f *FieldVal) SetBytes(b *[32]byte) uint32 { - // Pack the 256 total bits across the 10 uint32 words with a max of - // 26-bits per word. This could be done with a couple of for loops, +func (e *Element) SetBytes(b *[32]byte) uint32 { + // Pack the 256 total bits across the 10 uint32 limbs with at most + // 26-bits per limb. This could be done with a couple of for loops, // but this unrolled version is significantly faster. Benchmarks show // this is about 34 times faster than the variant which uses loops. - f.n[0] = uint32(b[31]) | uint32(b[30])<<8 | uint32(b[29])<<16 | + e.n[0] = uint32(b[31]) | uint32(b[30])<<8 | uint32(b[29])<<16 | (uint32(b[28])&twoBitsMask)<<24 - f.n[1] = uint32(b[28])>>2 | uint32(b[27])<<6 | uint32(b[26])<<14 | + e.n[1] = uint32(b[28])>>2 | uint32(b[27])<<6 | uint32(b[26])<<14 | (uint32(b[25])&fourBitsMask)<<22 - f.n[2] = uint32(b[25])>>4 | uint32(b[24])<<4 | uint32(b[23])<<12 | + e.n[2] = uint32(b[25])>>4 | uint32(b[24])<<4 | uint32(b[23])<<12 | (uint32(b[22])&sixBitsMask)<<20 - f.n[3] = uint32(b[22])>>6 | uint32(b[21])<<2 | uint32(b[20])<<10 | + e.n[3] = uint32(b[22])>>6 | uint32(b[21])<<2 | uint32(b[20])<<10 | uint32(b[19])<<18 - f.n[4] = uint32(b[18]) | uint32(b[17])<<8 | uint32(b[16])<<16 | + e.n[4] = uint32(b[18]) | uint32(b[17])<<8 | uint32(b[16])<<16 | (uint32(b[15])&twoBitsMask)<<24 - f.n[5] = uint32(b[15])>>2 | uint32(b[14])<<6 | uint32(b[13])<<14 | + e.n[5] = uint32(b[15])>>2 | uint32(b[14])<<6 | uint32(b[13])<<14 | (uint32(b[12])&fourBitsMask)<<22 - f.n[6] = uint32(b[12])>>4 | uint32(b[11])<<4 | uint32(b[10])<<12 | + e.n[6] = uint32(b[12])>>4 | uint32(b[11])<<4 | uint32(b[10])<<12 | (uint32(b[9])&sixBitsMask)<<20 - f.n[7] = uint32(b[9])>>6 | uint32(b[8])<<2 | uint32(b[7])<<10 | + e.n[7] = uint32(b[9])>>6 | uint32(b[8])<<2 | uint32(b[7])<<10 | uint32(b[6])<<18 - f.n[8] = uint32(b[5]) | uint32(b[4])<<8 | uint32(b[3])<<16 | + e.n[8] = uint32(b[5]) | uint32(b[4])<<8 | uint32(b[3])<<16 | (uint32(b[2])&twoBitsMask)<<24 - f.n[9] = uint32(b[2])>>2 | uint32(b[1])<<6 | uint32(b[0])<<14 + e.n[9] = uint32(b[2])>>2 | uint32(b[1])<<6 | uint32(b[0])<<14 - // The intuition here is that the field value is greater than the prime if - // one of the higher individual words is greater than corresponding word of - // the prime and all higher words in the field value are equal to their - // corresponding word of the prime. Since this type is modulo the prime, + // The intuition here is that the element is greater than the prime if one + // of the higher individual limbs is greater than corresponding limb of the + // prime and all higher limbs in the element are equal to their + // corresponding limb of the prime. Since this type is modulo the prime, // being equal is also an overflow back to 0. // // Note that because the input is 32 bytes and it was just packed into the - // field representation, the only words that can possibly be greater are + // internal representation, the only limbs that can possibly be greater are // zero and one, because ceil(log_2(2^256 - 1 - P)) = 33 bits max and the - // internal field representation encodes 26 bits with each word. + // internal representation encodes 26 bits with each limb. // - // Thus, there is no need to test if the upper words of the field value - // exceeds them, hence, only equality is checked for them. - highWordsEq := arith.ConstantTimeEq(f.n[9], fieldPrimeWordNine) - highWordsEq &= arith.ConstantTimeEq(f.n[8], fieldPrimeWordEight) - highWordsEq &= arith.ConstantTimeEq(f.n[7], fieldPrimeWordSeven) - highWordsEq &= arith.ConstantTimeEq(f.n[6], fieldPrimeWordSix) - highWordsEq &= arith.ConstantTimeEq(f.n[5], fieldPrimeWordFive) - highWordsEq &= arith.ConstantTimeEq(f.n[4], fieldPrimeWordFour) - highWordsEq &= arith.ConstantTimeEq(f.n[3], fieldPrimeWordThree) - highWordsEq &= arith.ConstantTimeEq(f.n[2], fieldPrimeWordTwo) - overflow := highWordsEq & arith.ConstantTimeGreater(f.n[1], fieldPrimeWordOne) - highWordsEq &= arith.ConstantTimeEq(f.n[1], fieldPrimeWordOne) - overflow |= highWordsEq & arith.ConstantTimeGreaterOrEq(f.n[0], fieldPrimeWordZero) + // Thus, there is no need to test if the upper limbs of the element exceeds + // them, hence, only equality is checked for them. + highLimbsEq := arith.ConstantTimeEq(e.n[9], fieldPrimeLimb9) + highLimbsEq &= arith.ConstantTimeEq(e.n[8], fieldPrimeLimb8) + highLimbsEq &= arith.ConstantTimeEq(e.n[7], fieldPrimeLimb7) + highLimbsEq &= arith.ConstantTimeEq(e.n[6], fieldPrimeLimb6) + highLimbsEq &= arith.ConstantTimeEq(e.n[5], fieldPrimeLimb5) + highLimbsEq &= arith.ConstantTimeEq(e.n[4], fieldPrimeLimb4) + highLimbsEq &= arith.ConstantTimeEq(e.n[3], fieldPrimeLimb3) + highLimbsEq &= arith.ConstantTimeEq(e.n[2], fieldPrimeLimb2) + overflow := highLimbsEq & arith.ConstantTimeGreater(e.n[1], fieldPrimeLimb1) + highLimbsEq &= arith.ConstantTimeEq(e.n[1], fieldPrimeLimb1) + overflow |= highLimbsEq & arith.ConstantTimeGreaterOrEq(e.n[0], fieldPrimeLimb0) return overflow } @@ -307,8 +309,8 @@ func zeroArray32(b *[32]byte) { // SetByteSlice interprets the provided slice as a 256-bit big-endian unsigned // integer (meaning it is truncated to the first 32 bytes), packs it into the -// internal field value representation, and returns whether or not the resulting -// truncated 256-bit integer is greater than or equal to the field prime (aka it +// internal representation, and returns whether or not the resulting truncated +// 256-bit integer is greater than or equal to the field prime (aka it // overflowed) in constant time. // // Note that since passing a slice with more than 32 bytes is truncated, it is @@ -319,35 +321,34 @@ func zeroArray32(b *[32]byte) { // overflow behavior. // // Preconditions: None -// Output Normalized: Yes if no overflow, no otherwise +// Output Normalized: Yes when no overflow, No otherwise // Output Max Magnitude: 1 -func (f *FieldVal) SetByteSlice(b []byte) bool { +func (e *Element) SetByteSlice(b []byte) bool { var b32 [32]byte b = b[:arith.ConstantTimeMin(uint32(len(b)), 32)] copy(b32[:], b32[:32-len(b)]) copy(b32[32-len(b):], b) - result := f.SetBytes(&b32) + result := e.SetBytes(&b32) zeroArray32(&b32) return result != 0 } -// Normalize normalizes the internal field words into the desired range and -// performs fast modular reduction over the secp256k1 prime by making use of the -// special form of the prime in constant time. +// Normalize normalizes the internal limbs into the desired range and performs +// fast modular reduction over the secp256k1 prime by making use of the special +// form of the prime in constant time. // // Preconditions: None // Output Normalized: Yes // Output Max Magnitude: 1 -func (f *FieldVal) Normalize() *FieldVal { - // The field representation leaves 6 bits of overflow in each word so - // intermediate calculations can be performed without needing to - // propagate the carry to each higher word during the calculations. In - // order to normalize, we need to "compact" the full 256-bit value to - // the right while propagating any carries through to the high order - // word. +func (e *Element) Normalize() *Element { + // The internal representation leaves 6 bits of overflow in each limb so + // intermediate calculations can be performed without needing to propagate + // the carry to each higher limb during the calculations. In order to + // normalize, we need to "compact" the full 256-bit value to the right while + // propagating any carries through to the high order limb. // - // Since this field is doing arithmetic modulo the secp256k1 prime, we - // also need to perform modular reduction over the prime. + // Since this is doing arithmetic modulo the secp256k1 prime, we also need + // to perform modular reduction over the prime. // // Per [HAC] section 14.3.4: Reduction method of moduli of special form, // when the modulus is of the special form m = b^t - c, highly efficient @@ -356,50 +357,50 @@ func (f *FieldVal) Normalize() *FieldVal { // The secp256k1 prime is equivalent to 2^256 - 4294968273, so it fits // this criteria. // - // 4294968273 in field representation (base 2^26) is: + // 4294968273 in internal representation (base 2^26) is: // n[0] = 977 // n[1] = 64 // That is to say (2^26 * 64) + 977 = 4294968273 // - // The algorithm presented in the referenced section typically repeats - // until the quotient is zero. However, due to our field representation - // we already know to within one reduction how many times we would need - // to repeat as it's the uppermost bits of the high order word. Thus we - // can simply multiply the magnitude by the field representation of the - // prime and do a single iteration. After this step there might be an - // additional carry to bit 256 (bit 22 of the high order word). - t9 := f.n[9] + // The algorithm presented in the referenced section typically repeats until + // the quotient is zero. However, due to our internal representation we + // already know to within one reduction how many times we would need to + // repeat as it's the uppermost bits of the high order limb. Thus we can + // simply multiply the magnitude by the internal representation of the prime + // and do a single iteration. After this step there might be an additional + // carry to bit 256 (bit 22 of the high order limb). + t9 := e.n[9] m := t9 >> fieldMSBBits t9 &= fieldMSBMask - t0 := f.n[0] + m*977 - t1 := (t0 >> fieldBase) + f.n[1] + (m << 6) + t0 := e.n[0] + m*977 + t1 := (t0 >> fieldBase) + e.n[1] + (m << 6) t0 &= fieldBaseMask - t2 := (t1 >> fieldBase) + f.n[2] + t2 := (t1 >> fieldBase) + e.n[2] t1 &= fieldBaseMask - t3 := (t2 >> fieldBase) + f.n[3] + t3 := (t2 >> fieldBase) + e.n[3] t2 &= fieldBaseMask - t4 := (t3 >> fieldBase) + f.n[4] + t4 := (t3 >> fieldBase) + e.n[4] t3 &= fieldBaseMask - t5 := (t4 >> fieldBase) + f.n[5] + t5 := (t4 >> fieldBase) + e.n[5] t4 &= fieldBaseMask - t6 := (t5 >> fieldBase) + f.n[6] + t6 := (t5 >> fieldBase) + e.n[6] t5 &= fieldBaseMask - t7 := (t6 >> fieldBase) + f.n[7] + t7 := (t6 >> fieldBase) + e.n[7] t6 &= fieldBaseMask - t8 := (t7 >> fieldBase) + f.n[8] + t8 := (t7 >> fieldBase) + e.n[8] t7 &= fieldBaseMask t9 = (t8 >> fieldBase) + t9 t8 &= fieldBaseMask // At this point, the magnitude is guaranteed to be one, however, the - // value could still be greater than the prime if there was either a - // carry through to bit 256 (bit 22 of the higher order word) or the - // value is greater than or equal to the field characteristic. The + // element could still be greater than the prime if there was either a + // carry through to bit 256 (bit 22 of the higher order limb) or the + // element is greater than or equal to the field characteristic. The // following determines if either or these conditions are true and does // the final reduction in constant time. // // Also note that 'm' will be zero when neither of the aforementioned - // conditions are true and the value will not be changed when 'm' is zero. + // conditions are true and the element will not be changed when 'm' is zero. m = arith.ConstantTimeEq(t9, fieldMSBMask) m &= arith.ConstantTimeEq(t8&t7&t6&t5&t4&t3&t2, fieldBaseMask) m &= arith.ConstantTimeGreater(t1+64+((t0+977)>>fieldBase), fieldBaseMask) @@ -425,487 +426,482 @@ func (f *FieldVal) Normalize() *FieldVal { t8 &= fieldBaseMask t9 &= fieldMSBMask // Remove potential multiple of 2^256. - // Finally, set the normalized and reduced words. - f.n[0] = t0 - f.n[1] = t1 - f.n[2] = t2 - f.n[3] = t3 - f.n[4] = t4 - f.n[5] = t5 - f.n[6] = t6 - f.n[7] = t7 - f.n[8] = t8 - f.n[9] = t9 - return f + // Finally, set the normalized and reduced limbs. + e.n[0] = t0 + e.n[1] = t1 + e.n[2] = t2 + e.n[3] = t3 + e.n[4] = t4 + e.n[5] = t5 + e.n[6] = t6 + e.n[7] = t7 + e.n[8] = t8 + e.n[9] = t9 + return e } -// PutBytesUnchecked unpacks the field value to a 32-byte big-endian value -// directly into the passed byte slice in constant time. The target slice must -// have at least 32 bytes available or it will panic. +// PutBytesUnchecked unpacks the element to a 32-byte big-endian value directly +// into the passed byte slice in constant time. The target slice must have at +// least 32 bytes available or it will panic. // -// There is a similar function, [FieldVal.PutBytes], which unpacks the field -// value into a 32-byte array directly. This version is provided since it can -// be useful to write directly into part of a larger buffer without needing a +// There is a similar function, [Element.PutBytes], which unpacks the element +// into a 32-byte array directly. This version is provided since it can be +// useful to write directly into part of a larger buffer without needing a // separate allocation. // // Preconditions: -// - The field value MUST be normalized +// - The element MUST be normalized // - The target slice MUST have at least 32 bytes available -func (f *FieldVal) PutBytesUnchecked(b []byte) { - // Unpack the 256 total bits from the 10 uint32 words with a max of - // 26-bits per word. This could be done with a couple of for loops, +func (e *Element) PutBytesUnchecked(b []byte) { + // Unpack the 256 total bits from the 10 uint32 limbs with at most + // 26-bits per limb. This could be done with a couple of for loops, // but this unrolled version is a bit faster. Benchmarks show this is // about 10 times faster than the variant which uses loops. - b[31] = byte(f.n[0] & eightBitsMask) - b[30] = byte((f.n[0] >> 8) & eightBitsMask) - b[29] = byte((f.n[0] >> 16) & eightBitsMask) - b[28] = byte((f.n[0]>>24)&twoBitsMask | (f.n[1]&sixBitsMask)<<2) - b[27] = byte((f.n[1] >> 6) & eightBitsMask) - b[26] = byte((f.n[1] >> 14) & eightBitsMask) - b[25] = byte((f.n[1]>>22)&fourBitsMask | (f.n[2]&fourBitsMask)<<4) - b[24] = byte((f.n[2] >> 4) & eightBitsMask) - b[23] = byte((f.n[2] >> 12) & eightBitsMask) - b[22] = byte((f.n[2]>>20)&sixBitsMask | (f.n[3]&twoBitsMask)<<6) - b[21] = byte((f.n[3] >> 2) & eightBitsMask) - b[20] = byte((f.n[3] >> 10) & eightBitsMask) - b[19] = byte((f.n[3] >> 18) & eightBitsMask) - b[18] = byte(f.n[4] & eightBitsMask) - b[17] = byte((f.n[4] >> 8) & eightBitsMask) - b[16] = byte((f.n[4] >> 16) & eightBitsMask) - b[15] = byte((f.n[4]>>24)&twoBitsMask | (f.n[5]&sixBitsMask)<<2) - b[14] = byte((f.n[5] >> 6) & eightBitsMask) - b[13] = byte((f.n[5] >> 14) & eightBitsMask) - b[12] = byte((f.n[5]>>22)&fourBitsMask | (f.n[6]&fourBitsMask)<<4) - b[11] = byte((f.n[6] >> 4) & eightBitsMask) - b[10] = byte((f.n[6] >> 12) & eightBitsMask) - b[9] = byte((f.n[6]>>20)&sixBitsMask | (f.n[7]&twoBitsMask)<<6) - b[8] = byte((f.n[7] >> 2) & eightBitsMask) - b[7] = byte((f.n[7] >> 10) & eightBitsMask) - b[6] = byte((f.n[7] >> 18) & eightBitsMask) - b[5] = byte(f.n[8] & eightBitsMask) - b[4] = byte((f.n[8] >> 8) & eightBitsMask) - b[3] = byte((f.n[8] >> 16) & eightBitsMask) - b[2] = byte((f.n[8]>>24)&twoBitsMask | (f.n[9]&sixBitsMask)<<2) - b[1] = byte((f.n[9] >> 6) & eightBitsMask) - b[0] = byte((f.n[9] >> 14) & eightBitsMask) + b[31] = byte(e.n[0] & eightBitsMask) + b[30] = byte((e.n[0] >> 8) & eightBitsMask) + b[29] = byte((e.n[0] >> 16) & eightBitsMask) + b[28] = byte((e.n[0]>>24)&twoBitsMask | (e.n[1]&sixBitsMask)<<2) + b[27] = byte((e.n[1] >> 6) & eightBitsMask) + b[26] = byte((e.n[1] >> 14) & eightBitsMask) + b[25] = byte((e.n[1]>>22)&fourBitsMask | (e.n[2]&fourBitsMask)<<4) + b[24] = byte((e.n[2] >> 4) & eightBitsMask) + b[23] = byte((e.n[2] >> 12) & eightBitsMask) + b[22] = byte((e.n[2]>>20)&sixBitsMask | (e.n[3]&twoBitsMask)<<6) + b[21] = byte((e.n[3] >> 2) & eightBitsMask) + b[20] = byte((e.n[3] >> 10) & eightBitsMask) + b[19] = byte((e.n[3] >> 18) & eightBitsMask) + b[18] = byte(e.n[4] & eightBitsMask) + b[17] = byte((e.n[4] >> 8) & eightBitsMask) + b[16] = byte((e.n[4] >> 16) & eightBitsMask) + b[15] = byte((e.n[4]>>24)&twoBitsMask | (e.n[5]&sixBitsMask)<<2) + b[14] = byte((e.n[5] >> 6) & eightBitsMask) + b[13] = byte((e.n[5] >> 14) & eightBitsMask) + b[12] = byte((e.n[5]>>22)&fourBitsMask | (e.n[6]&fourBitsMask)<<4) + b[11] = byte((e.n[6] >> 4) & eightBitsMask) + b[10] = byte((e.n[6] >> 12) & eightBitsMask) + b[9] = byte((e.n[6]>>20)&sixBitsMask | (e.n[7]&twoBitsMask)<<6) + b[8] = byte((e.n[7] >> 2) & eightBitsMask) + b[7] = byte((e.n[7] >> 10) & eightBitsMask) + b[6] = byte((e.n[7] >> 18) & eightBitsMask) + b[5] = byte(e.n[8] & eightBitsMask) + b[4] = byte((e.n[8] >> 8) & eightBitsMask) + b[3] = byte((e.n[8] >> 16) & eightBitsMask) + b[2] = byte((e.n[8]>>24)&twoBitsMask | (e.n[9]&sixBitsMask)<<2) + b[1] = byte((e.n[9] >> 6) & eightBitsMask) + b[0] = byte((e.n[9] >> 14) & eightBitsMask) } -// PutBytes unpacks the field value to a 32-byte big-endian value using the -// passed byte array in constant time. +// PutBytes unpacks the element to a 32-byte big-endian value using the passed +// byte array in constant time. // -// There is a similar function, [FieldVal.PutBytesUnchecked], which unpacks the -// field value into a slice that must have at least 32 bytes available. This +// There is a similar function, [Element.PutBytesUnchecked], which unpacks the +// element into a slice that must have at least 32 bytes available. This // version is provided since it can be useful to write directly into an array // that is type checked. // -// Alternatively, there is also [FieldVal.Bytes], which unpacks the field value -// into a new array and returns that which can sometimes be more ergonomic in +// Alternatively, there is also [Element.Bytes], which unpacks the element into +// a new array and returns that which can sometimes be more ergonomic in // applications that aren't concerned about an additional copy. // // Preconditions: -// - The field value MUST be normalized -func (f *FieldVal) PutBytes(b *[32]byte) { - f.PutBytesUnchecked(b[:]) +// - The element MUST be normalized +func (e *Element) PutBytes(b *[32]byte) { + e.PutBytesUnchecked(b[:]) } -// Bytes unpacks the field value to a 32-byte big-endian value in constant time. +// Bytes unpacks the element to a 32-byte big-endian value in constant time. // -// See [FieldVal.PutBytes] and [FieldVal.PutBytesUnchecked] for variants that +// See [Element.PutBytes] and [Element.PutBytesUnchecked] for variants that // allow an array or slice to be passed which can be useful to cut down on the // number of allocations by allowing the caller to reuse a buffer or write // directly into part of a larger buffer. // // Preconditions: -// - The field value MUST be normalized -func (f *FieldVal) Bytes() *[32]byte { +// - The element MUST be normalized +func (e *Element) Bytes() *[32]byte { b := new([32]byte) - f.PutBytesUnchecked(b[:]) + e.PutBytesUnchecked(b[:]) return b } -// IsZeroBit returns 1 when the field value is equal to zero or 0 otherwise in +// IsZeroBit returns 1 when the element is equal to zero or 0 otherwise in // constant time. // // Note that a bool is not used here because it is not possible in Go to convert // from a bool to numeric value in constant time and many constant-time -// operations require a numeric value. See [FieldVal.IsZero] for the version +// operations require a numeric value. See [Element.IsZero] for the version // that returns a bool. // // Preconditions: -// - The field value MUST be normalized -func (f *FieldVal) IsZeroBit() uint32 { - // The value can only be zero if no bits are set in any of the words. +// - The element MUST be normalized +func (e *Element) IsZeroBit() uint32 { + // The element can only be zero if no bits are set in any of the limbs. // This is a constant time implementation. - bits := f.n[0] | f.n[1] | f.n[2] | f.n[3] | f.n[4] | - f.n[5] | f.n[6] | f.n[7] | f.n[8] | f.n[9] + bits := e.n[0] | e.n[1] | e.n[2] | e.n[3] | e.n[4] | + e.n[5] | e.n[6] | e.n[7] | e.n[8] | e.n[9] return arith.ConstantTimeEq(bits, 0) } -// IsZero returns whether or not the field value is equal to zero in constant -// time. +// IsZero returns whether or not the element is equal to zero in constant time. // // Preconditions: -// - The field value MUST be normalized -func (f *FieldVal) IsZero() bool { - // The value can only be zero if no bits are set in any of the words. +// - The element MUST be normalized +func (e *Element) IsZero() bool { + // The element can only be zero if no bits are set in any of the limbs. // This is a constant time implementation. - bits := f.n[0] | f.n[1] | f.n[2] | f.n[3] | f.n[4] | - f.n[5] | f.n[6] | f.n[7] | f.n[8] | f.n[9] + bits := e.n[0] | e.n[1] | e.n[2] | e.n[3] | e.n[4] | + e.n[5] | e.n[6] | e.n[7] | e.n[8] | e.n[9] return bits == 0 } -// IsOneBit returns 1 when the field value is equal to one or 0 otherwise in +// IsOneBit returns 1 when the element is equal to one or 0 otherwise in // constant time. // // Note that a bool is not used here because it is not possible in Go to convert // from a bool to numeric value in constant time and many constant-time -// operations require a numeric value. See [FieldVal.IsOne] for the version +// operations require a numeric value. See [Element.IsOne] for the version // that returns a bool. // // Preconditions: -// - The field value MUST be normalized -func (f *FieldVal) IsOneBit() uint32 { - // The value can only be one if the single lowest significant bit is set in - // the first word and no other bits are set in any of the other words. +// - The element MUST be normalized +func (e *Element) IsOneBit() uint32 { + // The element can only be one if the single lowest significant bit is set in + // the first limb and no other bits are set in any of the other limbs. // This is a constant time implementation. - bits := (f.n[0] ^ 1) | f.n[1] | f.n[2] | f.n[3] | f.n[4] | f.n[5] | - f.n[6] | f.n[7] | f.n[8] | f.n[9] + bits := (e.n[0] ^ 1) | e.n[1] | e.n[2] | e.n[3] | e.n[4] | e.n[5] | + e.n[6] | e.n[7] | e.n[8] | e.n[9] return arith.ConstantTimeEq(bits, 0) } -// IsOne returns whether or not the field value is equal to one in constant -// time. +// IsOne returns whether or not the element is equal to one in constant time. // // Preconditions: -// - The field value MUST be normalized -func (f *FieldVal) IsOne() bool { - // The value can only be one if the single lowest significant bit is set in - // the first word and no other bits are set in any of the other words. +// - The element MUST be normalized +func (e *Element) IsOne() bool { + // The element can only be one if the single lowest significant bit is set + // in the first limb and no other bits are set in any of the other limbs. // This is a constant time implementation. - bits := (f.n[0] ^ 1) | f.n[1] | f.n[2] | f.n[3] | f.n[4] | f.n[5] | - f.n[6] | f.n[7] | f.n[8] | f.n[9] + bits := (e.n[0] ^ 1) | e.n[1] | e.n[2] | e.n[3] | e.n[4] | e.n[5] | + e.n[6] | e.n[7] | e.n[8] | e.n[9] return bits == 0 } -// IsOddBit returns 1 when the field value is an odd number or 0 otherwise in +// IsOddBit returns 1 when the element is an odd number or 0 otherwise in // constant time. // // Note that a bool is not used here because it is not possible in Go to convert // from a bool to numeric value in constant time and many constant-time -// operations require a numeric value. See [FieldVal.IsOdd] for the version +// operations require a numeric value. See [Element.IsOdd] for the version // that returns a bool. // // Preconditions: -// - The field value MUST be normalized -func (f *FieldVal) IsOddBit() uint32 { +// - The element MUST be normalized +func (e *Element) IsOddBit() uint32 { // Only odd numbers have the bottom bit set. - return f.n[0] & 1 + return e.n[0] & 1 } -// IsOdd returns whether or not the field value is an odd number in constant -// time. +// IsOdd returns whether or not the element is an odd number in constant time. // // Preconditions: -// - The field value MUST be normalized -func (f *FieldVal) IsOdd() bool { +// - The element MUST be normalized +func (e *Element) IsOdd() bool { // Only odd numbers have the bottom bit set. - return f.n[0]&1 == 1 + return e.n[0]&1 == 1 } -// Equals returns whether or not the two field values are the same in constant -// time. +// Equals returns whether or not the two elements are the same in constant time. // // Preconditions: -// - Both field values being compared MUST be normalized -func (f *FieldVal) Equals(val *FieldVal) bool { - // Xor only sets bits when they are different, so the two field values - // can only be the same if no bits are set after xoring each word. - // This is a constant time implementation. - bits := (f.n[0] ^ val.n[0]) | (f.n[1] ^ val.n[1]) | (f.n[2] ^ val.n[2]) | - (f.n[3] ^ val.n[3]) | (f.n[4] ^ val.n[4]) | (f.n[5] ^ val.n[5]) | - (f.n[6] ^ val.n[6]) | (f.n[7] ^ val.n[7]) | (f.n[8] ^ val.n[8]) | - (f.n[9] ^ val.n[9]) +// - Both elements being compared MUST be normalized +func (e *Element) Equals(val *Element) bool { + // Xor only sets bits when they are different, so the two elements can only + // be the same if no bits are set after xoring each limb. This is a + // constant time implementation. + bits := (e.n[0] ^ val.n[0]) | (e.n[1] ^ val.n[1]) | (e.n[2] ^ val.n[2]) | + (e.n[3] ^ val.n[3]) | (e.n[4] ^ val.n[4]) | (e.n[5] ^ val.n[5]) | + (e.n[6] ^ val.n[6]) | (e.n[7] ^ val.n[7]) | (e.n[8] ^ val.n[8]) | + (e.n[9] ^ val.n[9]) return bits == 0 } -// NegateVal negates the passed value and stores the result in f in constant -// time. The caller must provide the maximum magnitude of the passed value for -// a correct result. +// NegateVal negates the passed element and stores the result in e in constant +// time. The caller must provide the maximum magnitude of the passed element +// for a correct result. // -// The field value is returned to support chaining. This enables syntax like: -// f.NegateVal(f2).AddInt(1) so that f = -f2 + 1. +// The element is returned to support chaining. This enables syntax like: +// e.NegateVal(e2).AddInt(1) so that e = -e2 + 1. // // Preconditions: // - The max magnitude MUST be 31 // Output Normalized: No // Output Max Magnitude: Input magnitude + 1 -func (f *FieldVal) NegateVal(val *FieldVal, magnitude uint32) *FieldVal { - // Negation in the field is just the prime minus the value. However, - // in order to allow negation against a field value without having to - // normalize/reduce it first, multiply by the magnitude (that is how - // "far" away it is from the normalized value) to adjust. Also, since - // negating a value pushes it one more order of magnitude away from the +func (e *Element) NegateVal(val *Element, magnitude uint32) *Element { + // Negation in the field is just the prime minus the element. However, in + // order to allow negation against an element without having to + // normalize/reduce it first, multiply by the magnitude (that is how "far" + // away it is from its normalized representation) to adjust. Also, since + // negating an element pushes it one more order of magnitude away from the // normalized range, add 1 to compensate. // // For some intuition here, imagine you're performing mod 12 arithmetic - // (picture a clock) and you are negating the number 7. So you start at - // 12 (which is of course 0 under mod 12) and count backwards (left on - // the clock) 7 times to arrive at 5. Notice this is just 12-7 = 5. - // Now, assume you're starting with 19, which is a number that is - // already larger than the modulus and congruent to 7 (mod 12). When a - // value is already in the desired range, its magnitude is 1. Since 19 - // is an additional "step", its magnitude (mod 12) is 2. Since any - // multiple of the modulus is congruent to zero (mod m), the answer can - // be shortcut by simply multiplying the magnitude by the modulus and - // subtracting. Keeping with the example, this would be (2*12)-19 = 5. - f.n[0] = (magnitude+1)*fieldPrimeWordZero - val.n[0] - f.n[1] = (magnitude+1)*fieldPrimeWordOne - val.n[1] - f.n[2] = (magnitude+1)*fieldBaseMask - val.n[2] - f.n[3] = (magnitude+1)*fieldBaseMask - val.n[3] - f.n[4] = (magnitude+1)*fieldBaseMask - val.n[4] - f.n[5] = (magnitude+1)*fieldBaseMask - val.n[5] - f.n[6] = (magnitude+1)*fieldBaseMask - val.n[6] - f.n[7] = (magnitude+1)*fieldBaseMask - val.n[7] - f.n[8] = (magnitude+1)*fieldBaseMask - val.n[8] - f.n[9] = (magnitude+1)*fieldMSBMask - val.n[9] - - return f + // (picture a clock) and you are negating the number 7. So you start at 12 + // (which is of course 0 under mod 12) and count backwards (left on the + // clock) 7 times to arrive at 5. Notice this is just 12-7 = 5. Now, + // assume you're starting with 19, which is a number that is already larger + // than the modulus and congruent to 7 (mod 12). When an element is already + // in the desired range, its magnitude is 1. Since 19 is an additional + // "step", its magnitude (mod 12) is 2. Since any multiple of the modulus + // is congruent to zero (mod m), the answer can be shortcut by simply + // multiplying the magnitude by the modulus and subtracting. Keeping with + // the example, this would be (2*12)-19 = 5. + e.n[0] = (magnitude+1)*fieldPrimeLimb0 - val.n[0] + e.n[1] = (magnitude+1)*fieldPrimeLimb1 - val.n[1] + e.n[2] = (magnitude+1)*fieldBaseMask - val.n[2] + e.n[3] = (magnitude+1)*fieldBaseMask - val.n[3] + e.n[4] = (magnitude+1)*fieldBaseMask - val.n[4] + e.n[5] = (magnitude+1)*fieldBaseMask - val.n[5] + e.n[6] = (magnitude+1)*fieldBaseMask - val.n[6] + e.n[7] = (magnitude+1)*fieldBaseMask - val.n[7] + e.n[8] = (magnitude+1)*fieldBaseMask - val.n[8] + e.n[9] = (magnitude+1)*fieldMSBMask - val.n[9] + + return e } -// Negate negates the field value in constant time. The existing field value is -// modified. The caller must provide the maximum magnitude of the field value -// for a correct result. +// Negate negates the element in constant time. The existing element is +// modified. The caller must provide the maximum magnitude of the element for a +// correct result. // -// The field value is returned to support chaining. This enables syntax like: -// f.Negate().AddInt(1) so that f = -f + 1. +// The element is returned to support chaining. This enables syntax like: +// e.Negate().AddInt(1) so that e = -e + 1. // // Preconditions: // - The max magnitude MUST be 31 // Output Normalized: No // Output Max Magnitude: Input magnitude + 1 -func (f *FieldVal) Negate(magnitude uint32) *FieldVal { - return f.NegateVal(f, magnitude) +func (e *Element) Negate(magnitude uint32) *Element { + return e.NegateVal(e, magnitude) } -// AddInt adds the passed integer to the existing field value and stores the -// result in f in constant time. This is a convenience function since it is -// fairly common to perform some arithmetic with small native integers. +// AddInt adds the passed integer to the existing element and stores the result +// in e in constant time. This is a convenience function since it is fairly +// common to perform some arithmetic with small native integers. // -// The field value is returned to support chaining. This enables syntax like: -// f.AddInt(1).Add(f2) so that f = f + 1 + f2. +// The element is returned to support chaining. This enables syntax like: +// e.AddInt(1).Add(e2) so that e = e + 1 + e2. // // Preconditions: -// - The field value MUST have a max magnitude of 31 -// - The integer MUST be a max of 32767 +// - The element MUST have a max magnitude of 31 +// - The integer MUST be at most 32767 // Output Normalized: No -// Output Max Magnitude: Existing field magnitude + 1 -func (f *FieldVal) AddInt(ui uint16) *FieldVal { - // Since the field representation intentionally provides overflow bits, - // it's ok to use carryless addition as the carry bit is safely part of - // the word and will be normalized out. - f.n[0] += uint32(ui) - - return f +// Output Max Magnitude: Existing element magnitude + 1 +func (e *Element) AddInt(ui uint16) *Element { + // Since the internal representation intentionally provides overflow bits, + // it's ok to use carryless addition as the carry bit is safely part of the + // limb and will be normalized out. + e.n[0] += uint32(ui) + + return e } -// Add adds the passed value to the existing field value and stores the result -// in f in constant time. +// Add adds the passed element to the existing element and stores the result in +// e in constant time. // -// The field value is returned to support chaining. This enables syntax like: -// f.Add(f2).AddInt(1) so that f = f + f2 + 1. +// The element is returned to support chaining. This enables syntax like: +// e.Add(e2).AddInt(1) so that e = e + e2 + 1. // // Preconditions: -// - The sum of the magnitudes of the two field values MUST be a max of 32 +// - The sum of the magnitudes of the two elements MUST be at most 32 // Output Normalized: No -// Output Max Magnitude: Sum of the magnitude of the two individual field values -func (f *FieldVal) Add(val *FieldVal) *FieldVal { - // Since the field representation intentionally provides overflow bits, - // it's ok to use carryless addition as the carry bit is safely part of - // each word and will be normalized out. This could obviously be done - // in a loop, but the unrolled version is faster. - f.n[0] += val.n[0] - f.n[1] += val.n[1] - f.n[2] += val.n[2] - f.n[3] += val.n[3] - f.n[4] += val.n[4] - f.n[5] += val.n[5] - f.n[6] += val.n[6] - f.n[7] += val.n[7] - f.n[8] += val.n[8] - f.n[9] += val.n[9] - - return f +// Output Max Magnitude: Sum of the magnitude of the two individual elements +func (e *Element) Add(val *Element) *Element { + // Since the internal representation intentionally provides overflow bits, + // it's ok to use carryless addition as the carry bit is safely part of each + // limb and will be normalized out. This could obviously be done in a loop, + // but the unrolled version is faster. + e.n[0] += val.n[0] + e.n[1] += val.n[1] + e.n[2] += val.n[2] + e.n[3] += val.n[3] + e.n[4] += val.n[4] + e.n[5] += val.n[5] + e.n[6] += val.n[6] + e.n[7] += val.n[7] + e.n[8] += val.n[8] + e.n[9] += val.n[9] + + return e } -// Add2 adds the passed two field values together and stores the result in f in +// Add2 adds the passed two elements together and stores the result in e in // constant time. // -// The field value is returned to support chaining. This enables syntax like: -// f3.Add2(f, f2).AddInt(1) so that f3 = f + f2 + 1. +// The element is returned to support chaining. This enables syntax like: +// e3.Add2(e, e2).AddInt(1) so that e3 = e + e2 + 1. // // Preconditions: -// - The sum of the magnitudes of the two field values MUST be a max of 32 +// - The sum of the magnitudes of the two elements MUST be at most 32 // Output Normalized: No -// Output Max Magnitude: Sum of the magnitude of the two field values -func (f *FieldVal) Add2(val *FieldVal, val2 *FieldVal) *FieldVal { - // Since the field representation intentionally provides overflow bits, - // it's ok to use carryless addition as the carry bit is safely part of - // each word and will be normalized out. This could obviously be done - // in a loop, but the unrolled version is faster. - f.n[0] = val.n[0] + val2.n[0] - f.n[1] = val.n[1] + val2.n[1] - f.n[2] = val.n[2] + val2.n[2] - f.n[3] = val.n[3] + val2.n[3] - f.n[4] = val.n[4] + val2.n[4] - f.n[5] = val.n[5] + val2.n[5] - f.n[6] = val.n[6] + val2.n[6] - f.n[7] = val.n[7] + val2.n[7] - f.n[8] = val.n[8] + val2.n[8] - f.n[9] = val.n[9] + val2.n[9] - - return f +// Output Max Magnitude: Sum of the magnitude of the two elements +func (e *Element) Add2(val *Element, val2 *Element) *Element { + // Since the internal representation intentionally provides overflow bits, + // it's ok to use carryless addition as the carry bit is safely part of each + // limb and will be normalized out. This could obviously be done in a loop, + // but the unrolled version is faster. + e.n[0] = val.n[0] + val2.n[0] + e.n[1] = val.n[1] + val2.n[1] + e.n[2] = val.n[2] + val2.n[2] + e.n[3] = val.n[3] + val2.n[3] + e.n[4] = val.n[4] + val2.n[4] + e.n[5] = val.n[5] + val2.n[5] + e.n[6] = val.n[6] + val2.n[6] + e.n[7] = val.n[7] + val2.n[7] + e.n[8] = val.n[8] + val2.n[8] + e.n[9] = val.n[9] + val2.n[9] + + return e } -// MulBy2 multiplies the field value by 2 and stores the result in f in constant -// time. Note that this function can overflow if multiplying the value by any -// of the individual words exceeds a max uint32. Therefore it is important that -// the caller ensures no overflows will occur before using this function. +// MulBy2 multiplies the element by 2 and stores the result in e in constant +// time. Note that this function can overflow if multiplying the element causes +// any individual limb to overflow uint32. Therefore it is important that the +// caller ensures no overflows will occur before using this function. // -// The field value is returned to support chaining. This enables syntax like: -// f.MulBy2().Add(f2) so that f = 2 * f + f2. +// The element is returned to support chaining. This enables syntax like: +// e.MulBy2().Add(e2) so that e = 2 * e + e2. // // Preconditions: -// - The field value magnitude multiplied by 2 val MUST be a max of 32 +// - The element magnitude multiplied by 2 MUST be at most 32 // Output Normalized: No -// Output Max Magnitude: Existing field magnitude times 2 -func (f *FieldVal) MulBy2() *FieldVal { - return f.MulInt(2) +// Output Max Magnitude: Existing element magnitude times 2 +func (e *Element) MulBy2() *Element { + return e.MulInt(2) } -// MulBy3 multiplies the field value by 3 and stores the result in f in constant -// time. Note that this function can overflow if multiplying the value by any -// of the individual words exceeds a max uint32. Therefore it is important that -// the caller ensures no overflows will occur before using this function. +// MulBy3 multiplies the element by 3 and stores the result in e in constant +// time. Note that this function can overflow if multiplying the element causes +// any individual limb to overflow uint32. Therefore it is important that the +// caller ensures no overflows will occur before using this function. // -// The field value is returned to support chaining. This enables syntax like: -// f.MulBy3().Add(f2) so that f = 3 * f + f2. +// The element is returned to support chaining. This enables syntax like: +// e.MulBy3().Add(e2) so that e = 3 * e + e2. // // Preconditions: -// - The field value magnitude multiplied by 3 val MUST be a max of 32 +// - The element magnitude multiplied by 3 MUST be at most 32 // Output Normalized: No -// Output Max Magnitude: Existing field magnitude times 3 -func (f *FieldVal) MulBy3() *FieldVal { - return f.MulInt(3) +// Output Max Magnitude: Existing element magnitude times 3 +func (e *Element) MulBy3() *Element { + return e.MulInt(3) } -// MulBy4 multiplies the field value by 4 and stores the result in f in constant -// time. Note that this function can overflow if multiplying the value by any -// of the individual words exceeds a max uint32. Therefore it is important that -// the caller ensures no overflows will occur before using this function. +// MulBy4 multiplies the element by 4 and stores the result in e in constant +// time. Note that this function can overflow if multiplying the element causes +// any individual limb to overflow uint32. Therefore it is important that the +// caller ensures no overflows will occur before using this function. // -// The field value is returned to support chaining. This enables syntax like: -// f.MulBy4().Add(f2) so that f = 4 * f + f2. +// The element is returned to support chaining. This enables syntax like: +// e.MulBy4().Add(e2) so that e = 4 * e + e2. // // Preconditions: -// - The field value magnitude multiplied by 4 val MUST be a max of 32 +// - The element magnitude multiplied by 4 MUST be at most 32 // Output Normalized: No -// Output Max Magnitude: Existing field magnitude times 4 -func (f *FieldVal) MulBy4() *FieldVal { - return f.MulInt(4) +// Output Max Magnitude: Existing element magnitude times 4 +func (e *Element) MulBy4() *Element { + return e.MulInt(4) } -// MulBy8 multiplies the field value by 8 and stores the result in f in constant -// time. Note that this function can overflow if multiplying the value by any -// of the individual words exceeds a max uint32. Therefore it is important that -// the caller ensures no overflows will occur before using this function. +// MulBy8 multiplies the element by 8 and stores the result in e in constant +// time. Note that this function can overflow if multiplying the element causes +// any individual limb to overflow uint32. Therefore it is important that the +// caller ensures no overflows will occur before using this function. // -// The field value is returned to support chaining. This enables syntax like: -// f.MulBy8().Add(f2) so that f = 8 * f + f2. +// The element is returned to support chaining. This enables syntax like: +// e.MulBy8().Add(e2) so that e = 8 * e + e2. // // Preconditions: -// - The field value magnitude multiplied by 8 val MUST be a max of 32 +// - The element magnitude multiplied by 8 MUST be at most 32 // Output Normalized: No -// Output Max Magnitude: Existing field magnitude times 8 -func (f *FieldVal) MulBy8() *FieldVal { - return f.MulInt(8) +// Output Max Magnitude: Existing element times 8 +func (e *Element) MulBy8() *Element { + return e.MulInt(8) } -// MulInt multiplies the field value by the passed int and stores the result in -// f in constant time. Note that this function can overflow if multiplying the -// value by any of the individual words exceeds a max uint32. Therefore it is +// MulInt multiplies the element by the passed int and stores the result in e in +// constant time. Note that this function can overflow if multiplying the +// element causes any individual limb to overflow uint32. Therefore it is // important that the caller ensures no overflows will occur before using this // function. // // Callers should prefer using the specialized methods for multiplying by 2, 3, // 4, and 8, as they are commonly used in curve equations. // -// See [FieldVal.MulBy2], [FieldVal.MulBy3], [FieldVal.MulBy4], and -// [FieldVal.MulBy8] for the aforementioned specialized methods. +// See [Element.MulBy2], [Element.MulBy3], [Element.MulBy4], and +// [Element.MulBy8] for the aforementioned specialized methods. // -// The field value is returned to support chaining. This enables syntax like: -// f.MulInt(2).Add(f2) so that f = 2 * f + f2. +// The element is returned to support chaining. This enables syntax like: +// e.MulInt(2).Add(e2) so that e = 2 * e + e2. // // Preconditions: -// - The field value magnitude multiplied by given val MUST be a max of 32 +// - The element magnitude multiplied by given val MUST be at most 32 // Output Normalized: No -// Output Max Magnitude: Existing field magnitude times the provided integer val -func (f *FieldVal) MulInt(val uint8) *FieldVal { - // Since each word of the field representation can hold up to - // 32 - fieldBase extra bits which will be normalized out, it's safe - // to multiply each word without using a larger type or carry - // propagation so long as the values won't overflow a uint32. This - // could obviously be done in a loop, but the unrolled version is - // faster. +// Output Max Magnitude: Existing element magnitude times the provided integer val +func (e *Element) MulInt(val uint8) *Element { + // Since each limb of the internal representation can hold up to 32 - + // [fieldBase] extra bits which will be normalized out, it's safe to + // multiply each limb without using a larger type or carry propagation so + // long as the values won't overflow a uint32. This could obviously be done + // in a loop, but the unrolled version is faster. ui := uint32(val) - f.n[0] *= ui - f.n[1] *= ui - f.n[2] *= ui - f.n[3] *= ui - f.n[4] *= ui - f.n[5] *= ui - f.n[6] *= ui - f.n[7] *= ui - f.n[8] *= ui - f.n[9] *= ui - - return f + e.n[0] *= ui + e.n[1] *= ui + e.n[2] *= ui + e.n[3] *= ui + e.n[4] *= ui + e.n[5] *= ui + e.n[6] *= ui + e.n[7] *= ui + e.n[8] *= ui + e.n[9] *= ui + + return e } -// Mul multiplies the passed value to the existing field value and stores the -// result in f in constant time. Note that this function can overflow if -// multiplying any of the individual words exceeds a max uint32. In practice, -// this means the magnitude of either value involved in the multiplication must -// be a max of 8. +// Mul multiplies the passed element to the existing element and stores the +// result in e in constant time. Note that this function can overflow if +// multiplying causes any individual limb to overflow uint32. In practice, this +// means the magnitude of either element involved in the multiplication must be +// at most 8. // -// The field value is returned to support chaining. This enables syntax like: -// f.Mul(f2).AddInt(1) so that f = (f * f2) + 1. +// The element is returned to support chaining. This enables syntax like: +// e.Mul(e2).AddInt(1) so that e = (e * e2) + 1. // // Preconditions: -// - Both field values MUST have a max magnitude of 8 +// - Both elements MUST have a max magnitude of 8 // Output Normalized: No // Output Max Magnitude: 1 -func (f *FieldVal) Mul(val *FieldVal) *FieldVal { - return f.Mul2(f, val) +func (e *Element) Mul(val *Element) *Element { + return e.Mul2(e, val) } -// Mul2 multiplies the passed two field values together and stores the result in -// f in constant time. Note that this function can overflow if multiplying any -// of the individual words exceeds a max uint32. In practice, this means the -// magnitude of either value involved in the multiplication must be a max of 8. +// Mul2 multiplies the passed two elements together and stores the result in e +// in constant time. Note that this function can overflow if multiplying any of +// the individual limbs exceeds a max uint32. In practice, this means the +// magnitude of either element involved in the multiplication must be at most 8. // -// The field value is returned to support chaining. This enables syntax like: -// f3.Mul2(f, f2).AddInt(1) so that f3 = (f * f2) + 1. +// The element is returned to support chaining. This enables syntax like: +// e3.Mul2(e, e2).AddInt(1) so that e3 = (e * e2) + 1. // // Preconditions: -// - Both input field values MUST have a max magnitude of 8 +// - Both input elements MUST have a max magnitude of 8 // Output Normalized: No // Output Max Magnitude: 1 -func (f *FieldVal) Mul2(val *FieldVal, val2 *FieldVal) *FieldVal { +func (e *Element) Mul2(val *Element, val2 *Element) *Element { // This could be done with a couple of for loops and an array to store // the intermediate terms, but this unrolled version is significantly // faster. @@ -1097,24 +1093,24 @@ func (f *FieldVal) Mul2(val *FieldVal, val2 *FieldVal) *FieldVal { // The secp256k1 prime is equivalent to 2^256 - 4294968273, so it fits // this criteria. // - // 4294968273 in field representation (base 2^26) is: + // 4294968273 in the internal representation (base 2^26) is: // n[0] = 977 // n[1] = 64 // That is to say (2^26 * 64) + 977 = 4294968273 // - // Since each word is in base 26, the upper terms (t10 and up) start - // at 260 bits (versus the final desired range of 256 bits), so the - // field representation of 'c' from above needs to be adjusted for the - // extra 4 bits by multiplying it by 2^4 = 16. 4294968273 * 16 = - // 68719492368. Thus, the adjusted field representation of 'c' is: + // Since each limb is in base 26, the upper terms (t10 and up) start at 260 + // bits (versus the final desired range of 256 bits), so the internal + // representation of 'c' from above needs to be adjusted for the extra 4 + // bits by multiplying it by 2^4 = 16. 4294968273 * 16 = 68719492368. + // Thus, the adjusted internal representation of 'c' is: // n[0] = 977 * 16 = 15632 // n[1] = 64 * 16 = 1024 // That is to say (2^26 * 1024) + 15632 = 68719492368 // - // To reduce the final term, t19, the entire 'c' value is needed instead - // of only n[0] because there are no more terms left to handle n[1]. - // This means there might be some magnitude left in the upper bits that - // is handled below. + // To reduce the final term, t19, the entire 'c' value is needed instead of + // only n[0] because there are no more terms left to handle n[1]. This + // means there might be some magnitude left in the upper bits that is + // handled below. m = t0 + t10*15632 t0 = m & fieldBaseMask m = (m >> fieldBase) + t1 + t10*1024 + t11*15632 @@ -1137,7 +1133,7 @@ func (f *FieldVal) Mul2(val *FieldVal, val2 *FieldVal) *FieldVal { t9 = m & fieldMSBMask m >>= fieldMSBBits - // At this point, if the magnitude is greater than 0, the overall value + // At this point, if the magnitude is greater than 0, the overall element // is greater than the max possible 256-bit value. In particular, it is // "how many times larger" than the max value it is. // @@ -1145,46 +1141,46 @@ func (f *FieldVal) Mul2(val *FieldVal, val2 *FieldVal) *FieldVal { // quotient is zero. However, due to the above, we already know at // least how many times we would need to repeat as it's the value // currently in m. Thus we can simply multiply the magnitude by the - // field representation of the prime and do a single iteration. Notice + // internal representation of the prime and do a single iteration. Notice // that nothing will be changed when the magnitude is zero, so we could // skip this in that case, however always running regardless allows it // to run in constant time. The final result will be in the range // 0 <= result <= prime + (2^64 - c), so it is guaranteed to have a // magnitude of 1, but it is denormalized. d := t0 + m*977 - f.n[0] = uint32(d & fieldBaseMask) + e.n[0] = uint32(d & fieldBaseMask) d = (d >> fieldBase) + t1 + m*64 - f.n[1] = uint32(d & fieldBaseMask) - f.n[2] = uint32((d >> fieldBase) + t2) - f.n[3] = uint32(t3) - f.n[4] = uint32(t4) - f.n[5] = uint32(t5) - f.n[6] = uint32(t6) - f.n[7] = uint32(t7) - f.n[8] = uint32(t8) - f.n[9] = uint32(t9) - - return f + e.n[1] = uint32(d & fieldBaseMask) + e.n[2] = uint32((d >> fieldBase) + t2) + e.n[3] = uint32(t3) + e.n[4] = uint32(t4) + e.n[5] = uint32(t5) + e.n[6] = uint32(t6) + e.n[7] = uint32(t7) + e.n[8] = uint32(t8) + e.n[9] = uint32(t9) + + return e } -// SquareRootVal either calculates the square root of the passed value when it -// exists or the square root of the negation of the value when it does not exist -// and stores the result in f in constant time. The return flag is true when -// the calculated square root is for the passed value itself and false when it -// is for its negation. +// SquareRootVal either calculates the square root of the passed element when it +// exists or the square root of the negation of the element when it does not +// exist and stores the result in e in constant time. The return flag is true +// when the calculated square root is for the passed element itself and false +// when it is for its negation. // // Note that this function can overflow if multiplying any of the individual -// words exceeds a max uint32. In practice, this means the magnitude of the -// field must be a max of 8 to prevent overflow. The magnitude of the result +// limbs exceeds a max uint32. In practice, this means the magnitude of the +// element must be at most 8 to prevent overflow. The magnitude of the result // will be 1. // // Preconditions: -// - The input field value MUST have a max magnitude of 8 +// - The input element MUST have a max magnitude of 8 // Output Normalized: No // Output Max Magnitude: 1 -func (f *FieldVal) SquareRootVal(val *FieldVal) bool { +func (e *Element) SquareRootVal(val *Element) bool { // This uses the Tonelli-Shanks method for calculating the square root of - // the value when it exists. The key principles of the method follow. + // the element when it exists. The key principles of the method follow. // // Fermat's little theorem states that for a nonzero number 'a' and prime // 'p', a^(p-1) ≡ 1 (mod p). @@ -1206,15 +1202,15 @@ func (f *FieldVal) SquareRootVal(val *FieldVal) bool { // The Tonelli-Shanks method uses these facts along with factoring out // powers of two to solve a congruence that results in either the solution // when the square root exists or the square root of the negation of the - // value when it does not. In the case of primes that are ≡ 3 (mod 4), the - // possible solutions are r = ±a^((p+1)/4) (mod p). Therefore, either r^2 ≡ - // a (mod p) is true in which case ±r are the two solutions, or r^2 ≡ -a - // (mod p) in which case 'a' is a non-residue and there are no solutions. + // element when it does not. In the case of primes that are ≡ 3 (mod 4), + // the possible solutions are r = ±a^((p+1)/4) (mod p). Therefore, either + // r^2 ≡ a (mod p) is true in which case ±r are the two solutions, or r^2 ≡ + // -a (mod p) in which case 'a' is a non-residue and there are no solutions. // // The secp256k1 prime is ≡ 3 (mod 4), so this result applies. // // In other words, calculate a^((p+1)/4) and then square it and check it - // against the original value to determine if it is actually the square + // against the original element to determine if it is actually the square // root. // // In order to efficiently compute a^((p+1)/4), (p+1)/4 needs to be split @@ -1250,7 +1246,7 @@ func (f *FieldVal) SquareRootVal(val *FieldVal) bool { // => 2^1 2^[2] 2^3 2^6 2^9 2^11 2^[22] 2^44 2^88 2^176 2^220 2^[223] // // This has a cost of 254 field squarings and 13 field multiplications. - var a, a2, a3, a6, a9, a11, a22, a44, a88, a176, a220, a223 FieldVal + var a, a2, a3, a6, a9, a11, a22, a44, a88, a176, a220, a223 Element a.Set(val) a2.SquareVal(&a).Mul(&a) // a2 = a^(2^2 - 1) a3.SquareVal(&a2).Mul(&a) // a3 = a^(2^3 - 1) @@ -1312,54 +1308,54 @@ func (f *FieldVal) SquareRootVal(val *FieldVal) bool { a223.SquareVal(&a220).Square().Square() // a223 = a^(2^223 - 2^3) a223.Mul(&a3) // a223 = a^(2^223 - 1) - f.SquareVal(&a223).Square().Square().Square().Square() // f = a^(2^228 - 2^5) - f.Square().Square().Square().Square().Square() // f = a^(2^233 - 2^10) - f.Square().Square().Square().Square().Square() // f = a^(2^238 - 2^15) - f.Square().Square().Square().Square().Square() // f = a^(2^243 - 2^20) - f.Square().Square().Square() // f = a^(2^246 - 2^23) - f.Mul(&a22) // f = a^(2^246 - 2^22 - 1) - f.Square().Square().Square().Square().Square() // f = a^(2^251 - 2^27 - 2^5) - f.Square() // f = a^(2^252 - 2^28 - 2^6) - f.Mul(&a2) // f = a^(2^252 - 2^28 - 2^6 - 2^1 - 1) - f.Square().Square() // f = a^(2^254 - 2^30 - 2^8 - 2^3 - 2^2) + e.SquareVal(&a223).Square().Square().Square().Square() // e = a^(2^228 - 2^5) + e.Square().Square().Square().Square().Square() // e = a^(2^233 - 2^10) + e.Square().Square().Square().Square().Square() // e = a^(2^238 - 2^15) + e.Square().Square().Square().Square().Square() // e = a^(2^243 - 2^20) + e.Square().Square().Square() // e = a^(2^246 - 2^23) + e.Mul(&a22) // e = a^(2^246 - 2^22 - 1) + e.Square().Square().Square().Square().Square() // e = a^(2^251 - 2^27 - 2^5) + e.Square() // e = a^(2^252 - 2^28 - 2^6) + e.Mul(&a2) // e = a^(2^252 - 2^28 - 2^6 - 2^1 - 1) + e.Square().Square() // e = a^(2^254 - 2^30 - 2^8 - 2^3 - 2^2) // // = a^(2^254 - 2^30 - 244) // // = a^((p+1)/4) // Ensure the calculated result is actually the square root by squaring it - // and checking against the original value. - var sqr FieldVal - return sqr.SquareVal(f).Normalize().Equals(val.Normalize()) + // and checking against the original element. + var sqr Element + return sqr.SquareVal(e).Normalize().Equals(val.Normalize()) } -// Square squares the field value in constant time. The existing field value is +// Square squares the element in constant time. The existing element is // modified. Note that this function can overflow if multiplying any of the -// individual words exceeds a max uint32. In practice, this means the magnitude -// of the field must be a max of 8 to prevent overflow. +// individual limbs exceeds a max uint32. In practice, this means the magnitude +// of the element must be at most 8 to prevent overflow. // -// The field value is returned to support chaining. This enables syntax like: -// f.Square().Mul(f2) so that f = f^2 * f2. +// The element is returned to support chaining. This enables syntax like: +// e.Square().Mul(e2) so that e = e^2 * e2. // // Preconditions: -// - The field value MUST have a max magnitude of 8 +// - The element MUST have a max magnitude of 8 // Output Normalized: No // Output Max Magnitude: 1 -func (f *FieldVal) Square() *FieldVal { - return f.SquareVal(f) +func (e *Element) Square() *Element { + return e.SquareVal(e) } -// SquareVal squares the passed value and stores the result in f in constant +// SquareVal squares the passed element and stores the result in e in constant // time. Note that this function can overflow if multiplying any of the -// individual words exceeds a max uint32. In practice, this means the magnitude -// of the field being squared must be a max of 8 to prevent overflow. +// individual limbs exceeds a max uint32. In practice, this means the magnitude +// of the element being squared must be at most 8 to prevent overflow. // -// The field value is returned to support chaining. This enables syntax like: -// f3.SquareVal(f).Mul(f) so that f3 = f^2 * f = f^3. +// The element is returned to support chaining. This enables syntax like: +// e3.SquareVal(e).Mul(e) so that e3 = e^2 * e = e^3. // // Preconditions: -// - The input field value MUST have a max magnitude of 8 +// - The input element MUST have a max magnitude of 8 // Output Normalized: No // Output Max Magnitude: 1 -func (f *FieldVal) SquareVal(val *FieldVal) *FieldVal { +func (e *Element) SquareVal(val *Element) *Element { // This could be done with a couple of for loops and an array to store // the intermediate terms, but this unrolled version is significantly // faster. @@ -1504,16 +1500,16 @@ func (f *FieldVal) SquareVal(val *FieldVal) *FieldVal { // The secp256k1 prime is equivalent to 2^256 - 4294968273, so it fits // this criteria. // - // 4294968273 in field representation (base 2^26) is: + // 4294968273 in the internal representation (base 2^26) is: // n[0] = 977 // n[1] = 64 // That is to say (2^26 * 64) + 977 = 4294968273 // - // Since each word is in base 26, the upper terms (t10 and up) start - // at 260 bits (versus the final desired range of 256 bits), so the - // field representation of 'c' from above needs to be adjusted for the - // extra 4 bits by multiplying it by 2^4 = 16. 4294968273 * 16 = - // 68719492368. Thus, the adjusted field representation of 'c' is: + // Since each limb is in base 26, the upper terms (t10 and up) start at 260 + // bits (versus the final desired range of 256 bits), so the internal + // representation of 'c' from above needs to be adjusted for the extra 4 + // bits by multiplying it by 2^4 = 16. 4294968273 * 16 = 68719492368. + // Thus, the adjusted internal representation of 'c' is: // n[0] = 977 * 16 = 15632 // n[1] = 64 * 16 = 1024 // That is to say (2^26 * 1024) + 15632 = 68719492368 @@ -1544,47 +1540,47 @@ func (f *FieldVal) SquareVal(val *FieldVal) *FieldVal { t9 = m & fieldMSBMask m >>= fieldMSBBits - // At this point, if the magnitude is greater than 0, the overall value + // At this point, if the magnitude is greater than 0, the overall element // is greater than the max possible 256-bit value. In particular, it is // "how many times larger" than the max value it is. // // The algorithm presented in [HAC] section 14.3.4 repeats until the // quotient is zero. However, due to the above, we already know at - // least how many times we would need to repeat as it's the value + // least how many times we would need to repeat as it's the quantity // currently in m. Thus we can simply multiply the magnitude by the - // field representation of the prime and do a single iteration. Notice + // internal representation of the prime and do a single iteration. Notice // that nothing will be changed when the magnitude is zero, so we could // skip this in that case, however always running regardless allows it // to run in constant time. The final result will be in the range // 0 <= result <= prime + (2^64 - c), so it is guaranteed to have a // magnitude of 1, but it is denormalized. n := t0 + m*977 - f.n[0] = uint32(n & fieldBaseMask) + e.n[0] = uint32(n & fieldBaseMask) n = (n >> fieldBase) + t1 + m*64 - f.n[1] = uint32(n & fieldBaseMask) - f.n[2] = uint32((n >> fieldBase) + t2) - f.n[3] = uint32(t3) - f.n[4] = uint32(t4) - f.n[5] = uint32(t5) - f.n[6] = uint32(t6) - f.n[7] = uint32(t7) - f.n[8] = uint32(t8) - f.n[9] = uint32(t9) - - return f + e.n[1] = uint32(n & fieldBaseMask) + e.n[2] = uint32((n >> fieldBase) + t2) + e.n[3] = uint32(t3) + e.n[4] = uint32(t4) + e.n[5] = uint32(t5) + e.n[6] = uint32(t6) + e.n[7] = uint32(t7) + e.n[8] = uint32(t8) + e.n[9] = uint32(t9) + + return e } -// Inverse finds the modular multiplicative inverse of the field value in -// constant time. The existing field value is modified. +// Inverse finds the modular multiplicative inverse of the element in constant +// time. The existing element is modified. // -// The field value is returned to support chaining. This enables syntax like: -// f.Inverse().Mul(f2) so that f = f^-1 * f2. +// The element is returned to support chaining. This enables syntax like: +// e.Inverse().Mul(e2) so that e = e^-1 * e2. // // Preconditions: -// - The field value MUST have a max magnitude of 8 +// - The element MUST have a max magnitude of 8 // Output Normalized: No // Output Max Magnitude: 1 -func (f *FieldVal) Inverse() *FieldVal { +func (e *Element) Inverse() *Element { // Fermat's little theorem states that for a nonzero number 'a' and prime // 'p', a^(p-1) ≡ 1 (mod p). Multiplying both sides of the equation by the // multiplicative inverse a^-1 yields a^(p-2) ≡ a^-1 (mod p). Thus, a^(p-2) @@ -1626,8 +1622,8 @@ func (f *FieldVal) Inverse() *FieldVal { // => 2^[1] 2^[2] 2^3 2^6 2^9 2^11 2^[22] 2^44 2^88 2^176 2^220 2^[223] // // This has a cost of 255 field squarings and 15 field multiplications. - var a, a2, a3, a6, a9, a11, a22, a44, a88, a176, a220, a223 FieldVal - a.Set(f) + var a, a2, a3, a6, a9, a11, a22, a44, a88, a176, a220, a223 Element + a.Set(e) a2.SquareVal(&a).Mul(&a) // a2 = a^(2^2 - 1) a3.SquareVal(&a2).Mul(&a) // a3 = a^(2^3 - 1) a6.SquareVal(&a3).Square().Square() // a6 = a^(2^6 - 2^3) @@ -1688,27 +1684,26 @@ func (f *FieldVal) Inverse() *FieldVal { a223.SquareVal(&a220).Square().Square() // a223 = a^(2^223 - 2^3) a223.Mul(&a3) // a223 = a^(2^223 - 1) - f.SquareVal(&a223).Square().Square().Square().Square() // f = a^(2^228 - 2^5) - f.Square().Square().Square().Square().Square() // f = a^(2^233 - 2^10) - f.Square().Square().Square().Square().Square() // f = a^(2^238 - 2^15) - f.Square().Square().Square().Square().Square() // f = a^(2^243 - 2^20) - f.Square().Square().Square() // f = a^(2^246 - 2^23) - f.Mul(&a22) // f = a^(2^246 - 4194305) - f.Square().Square().Square().Square().Square() // f = a^(2^251 - 134217760) - f.Mul(&a) // f = a^(2^251 - 134217759) - f.Square().Square().Square() // f = a^(2^254 - 1073742072) - f.Mul(&a2) // f = a^(2^254 - 1073742069) - f.Square().Square() // f = a^(2^256 - 4294968276) - return f.Mul(&a) // f = a^(2^256 - 4294968275) = a^(p-2) + e.SquareVal(&a223).Square().Square().Square().Square() // e = a^(2^228 - 2^5) + e.Square().Square().Square().Square().Square() // e = a^(2^233 - 2^10) + e.Square().Square().Square().Square().Square() // e = a^(2^238 - 2^15) + e.Square().Square().Square().Square().Square() // e = a^(2^243 - 2^20) + e.Square().Square().Square() // e = a^(2^246 - 2^23) + e.Mul(&a22) // e = a^(2^246 - 4194305) + e.Square().Square().Square().Square().Square() // e = a^(2^251 - 134217760) + e.Mul(&a) // e = a^(2^251 - 134217759) + e.Square().Square().Square() // e = a^(2^254 - 1073742072) + e.Mul(&a2) // e = a^(2^254 - 1073742069) + e.Square().Square() // e = a^(2^256 - 4294968276) + return e.Mul(&a) // e = a^(2^256 - 4294968275) = a^(p-2) } -// IsGtOrEqPrimeMinusOrder returns whether or not the field value is greater -// than or equal to the field prime minus the secp256k1 group order in constant -// time. +// IsGtOrEqPrimeMinusOrder returns whether or not the element is greater than or +// equal to the field prime minus the secp256k1 group order in constant time. // // Preconditions: -// - The field value MUST be normalized -func (f *FieldVal) IsGtOrEqPrimeMinusOrder() bool { +// - The element MUST be normalized +func (e *Element) IsGtOrEqPrimeMinusOrder() bool { // The secp256k1 prime is equivalent to 2^256 - 4294968273 and the group // order is 2^256 - 432420386565659656852420866394968145599. Thus, // the prime minus the group order is: @@ -1717,7 +1712,7 @@ func (f *FieldVal) IsGtOrEqPrimeMinusOrder() bool { // In hex that is: // 0x00000000 00000000 00000000 00000001 45512319 50b75fc4 402da172 2fc9baee // - // Converting that to field representation (base 2^26) is: + // Converting that to the internal representation (base 2^26) is: // // n[0] = 0x03c9baee // n[1] = 0x03685c8b @@ -1727,46 +1722,46 @@ func (f *FieldVal) IsGtOrEqPrimeMinusOrder() bool { // // This can be verified with the following test code: // pMinusN := new(big.Int).Sub(curveParams.P, curveParams.N) - // var fv FieldVal - // fv.SetByteSlice(pMinusN.Bytes()) - // t.Logf("%x", fv.n) + // var v Element + // v.SetByteSlice(pMinusN.Bytes()) + // t.Logf("%x", v.n) // // Outputs: [3c9baee 3685c8b 1fc4402 6542dd 1455123 0 0 0 0 0] const ( - pMinusNWordZero = 0x03c9baee - pMinusNWordOne = 0x03685c8b - pMinusNWordTwo = 0x01fc4402 - pMinusNWordThree = 0x006542dd - pMinusNWordFour = 0x01455123 - pMinusNWordFive = 0x00000000 - pMinusNWordSix = 0x00000000 - pMinusNWordSeven = 0x00000000 - pMinusNWordEight = 0x00000000 - pMinusNWordNine = 0x00000000 + pMinusNLimb0 = 0x03c9baee + pMinusNLimb1 = 0x03685c8b + pMinusNLimb2 = 0x01fc4402 + pMinusNLimb3 = 0x006542dd + pMinusNLimb4 = 0x01455123 + pMinusNLimb5 = 0x00000000 + pMinusNLimb6 = 0x00000000 + pMinusNLimb7 = 0x00000000 + pMinusNLimb8 = 0x00000000 + pMinusNLimb9 = 0x00000000 ) - // The intuition here is that the value is greater than field prime minus - // the group order if one of the higher individual words is greater than the - // corresponding word and all higher words in the value are equal. - result := arith.ConstantTimeGreater(f.n[9], pMinusNWordNine) - highWordsEqual := arith.ConstantTimeEq(f.n[9], pMinusNWordNine) - result |= highWordsEqual & arith.ConstantTimeGreater(f.n[8], pMinusNWordEight) - highWordsEqual &= arith.ConstantTimeEq(f.n[8], pMinusNWordEight) - result |= highWordsEqual & arith.ConstantTimeGreater(f.n[7], pMinusNWordSeven) - highWordsEqual &= arith.ConstantTimeEq(f.n[7], pMinusNWordSeven) - result |= highWordsEqual & arith.ConstantTimeGreater(f.n[6], pMinusNWordSix) - highWordsEqual &= arith.ConstantTimeEq(f.n[6], pMinusNWordSix) - result |= highWordsEqual & arith.ConstantTimeGreater(f.n[5], pMinusNWordFive) - highWordsEqual &= arith.ConstantTimeEq(f.n[5], pMinusNWordFive) - result |= highWordsEqual & arith.ConstantTimeGreater(f.n[4], pMinusNWordFour) - highWordsEqual &= arith.ConstantTimeEq(f.n[4], pMinusNWordFour) - result |= highWordsEqual & arith.ConstantTimeGreater(f.n[3], pMinusNWordThree) - highWordsEqual &= arith.ConstantTimeEq(f.n[3], pMinusNWordThree) - result |= highWordsEqual & arith.ConstantTimeGreater(f.n[2], pMinusNWordTwo) - highWordsEqual &= arith.ConstantTimeEq(f.n[2], pMinusNWordTwo) - result |= highWordsEqual & arith.ConstantTimeGreater(f.n[1], pMinusNWordOne) - highWordsEqual &= arith.ConstantTimeEq(f.n[1], pMinusNWordOne) - result |= highWordsEqual & arith.ConstantTimeGreaterOrEq(f.n[0], pMinusNWordZero) + // The intuition here is that the element is greater than field prime minus + // the group order if one of the higher individual limbs is greater than the + // corresponding limb and all higher limbs in the element are equal. + result := arith.ConstantTimeGreater(e.n[9], pMinusNLimb9) + highLimbsEqual := arith.ConstantTimeEq(e.n[9], pMinusNLimb9) + result |= highLimbsEqual & arith.ConstantTimeGreater(e.n[8], pMinusNLimb8) + highLimbsEqual &= arith.ConstantTimeEq(e.n[8], pMinusNLimb8) + result |= highLimbsEqual & arith.ConstantTimeGreater(e.n[7], pMinusNLimb7) + highLimbsEqual &= arith.ConstantTimeEq(e.n[7], pMinusNLimb7) + result |= highLimbsEqual & arith.ConstantTimeGreater(e.n[6], pMinusNLimb6) + highLimbsEqual &= arith.ConstantTimeEq(e.n[6], pMinusNLimb6) + result |= highLimbsEqual & arith.ConstantTimeGreater(e.n[5], pMinusNLimb5) + highLimbsEqual &= arith.ConstantTimeEq(e.n[5], pMinusNLimb5) + result |= highLimbsEqual & arith.ConstantTimeGreater(e.n[4], pMinusNLimb4) + highLimbsEqual &= arith.ConstantTimeEq(e.n[4], pMinusNLimb4) + result |= highLimbsEqual & arith.ConstantTimeGreater(e.n[3], pMinusNLimb3) + highLimbsEqual &= arith.ConstantTimeEq(e.n[3], pMinusNLimb3) + result |= highLimbsEqual & arith.ConstantTimeGreater(e.n[2], pMinusNLimb2) + highLimbsEqual &= arith.ConstantTimeEq(e.n[2], pMinusNLimb2) + result |= highLimbsEqual & arith.ConstantTimeGreater(e.n[1], pMinusNLimb1) + highLimbsEqual &= arith.ConstantTimeEq(e.n[1], pMinusNLimb1) + result |= highLimbsEqual & arith.ConstantTimeGreaterOrEq(e.n[0], pMinusNLimb0) return result != 0 } diff --git a/dcrec/secp256k1/field10x26/element_bench_test.go b/dcrec/secp256k1/field10x26/element_bench_test.go index 11c34b106f..d795c9cc82 100644 --- a/dcrec/secp256k1/field10x26/element_bench_test.go +++ b/dcrec/secp256k1/field10x26/element_bench_test.go @@ -9,11 +9,11 @@ import ( "testing" ) -// BenchmarkFieldNormalize benchmarks how long it takes the internal field -// to perform normalization (which includes modular reduction) with [FieldVal]. -func BenchmarkFieldNormalize(b *testing.B) { +// BenchmarkElementNormalize benchmarks how long it takes the internal field +// to perform normalization (which includes modular reduction) with [Element]. +func BenchmarkElementNormalize(b *testing.B) { // The function is constant time so any value is fine. - f := &FieldVal{n: [10]uint32{ + e := &Element{n: [10]uint32{ 0x000148f6, 0x03ffffc0, 0x03ffffff, 0x03ffffff, 0x03ffffff, 0x03ffffff, 0x03ffffff, 0x03ffffff, 0x03ffffff, 0x00000007, }} @@ -21,7 +21,7 @@ func BenchmarkFieldNormalize(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - f.Normalize() + e.Normalize() } } @@ -40,18 +40,18 @@ func BenchmarkBigIntNegateModP(b *testing.B) { } } -// BenchmarkFieldNegate benchmarks calculating the additive inverse of an -// unsigned 256-bit big-endian integer modulo the field prime with [FieldVal]. -func BenchmarkFieldNegate(b *testing.B) { +// BenchmarkElementNegate benchmarks calculating the additive inverse of an +// unsigned 256-bit big-endian integer modulo the field prime with [Element]. +func BenchmarkElementNegate(b *testing.B) { // The function is constant time so any value is fine. valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal(valHex) + e := mustElement(valHex) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - var result FieldVal - _ = result.NegateVal(f, 1) + var result Element + _ = result.NegateVal(e, 1) } } @@ -71,85 +71,85 @@ func BenchmarkBigIntAddModP(b *testing.B) { } } -// BenchmarkFieldAdd benchmarks adding two unsigned 256-bit big-endian integers -// modulo the field prime with [FieldVal]. -func BenchmarkFieldAdd(b *testing.B) { +// BenchmarkElementAdd benchmarks adding two unsigned 256-bit big-endian +// integers modulo the field prime with [Element]. +func BenchmarkElementAdd(b *testing.B) { // The function is constant time so any values are fine. f1Hex := "d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab" f2Hex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f1 := mustFieldVal(f1Hex) - f2 := mustFieldVal(f2Hex) + f1 := mustElement(f1Hex) + f2 := mustElement(f2Hex) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - var sum FieldVal + var sum Element sum.Add2(f1, f2) } } -// BenchmarkFieldMulBy2 benchmarks multiplying an unsigned 256-bit big-endian -// integer by 2 with [FieldVal.MulBy2]. -func BenchmarkFieldMulBy2(b *testing.B) { - fHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal(fHex) +// BenchmarkElementMulBy2 benchmarks multiplying an unsigned 256-bit big-endian +// integer by 2 with [Element.MulBy2]. +func BenchmarkElementMulBy2(b *testing.B) { + valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(valHex) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - f.MulBy2() + e.MulBy2() } } -// BenchmarkFieldMulBy3 benchmarks multiplying an unsigned 256-bit big-endian -// integer by 3 with [FieldVal.MulBy3]. -func BenchmarkFieldMulBy3(b *testing.B) { - fHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal(fHex) +// BenchmarkElementMulBy3 benchmarks multiplying an unsigned 256-bit big-endian +// integer by 3 with [Element.MulBy3]. +func BenchmarkElementMulBy3(b *testing.B) { + valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(valHex) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - f.MulBy3() + e.MulBy3() } } -// BenchmarkFieldMulBy4 benchmarks multiplying an unsigned 256-bit big-endian -// integer by 4 with [FieldVal.MulBy4]. -func BenchmarkFieldMulBy4(b *testing.B) { - fHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal(fHex) +// BenchmarkElementMulBy4 benchmarks multiplying an unsigned 256-bit big-endian +// integer by 4 with [Element.MulBy4]. +func BenchmarkElementMulBy4(b *testing.B) { + valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(valHex) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - f.MulBy4() + e.MulBy4() } } -// BenchmarkFieldMulBy8 benchmarks multiplying an unsigned 256-bit big-endian -// integer by 8 with [FieldVal.MulBy8]. -func BenchmarkFieldMulBy8(b *testing.B) { - fHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal(fHex) +// BenchmarkElementMulBy8 benchmarks multiplying an unsigned 256-bit big-endian +// integer by 8 with [Element.MulBy8]. +func BenchmarkElementMulBy8(b *testing.B) { + valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(valHex) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - f.MulBy8() + e.MulBy8() } } -// BenchmarkFieldMulInt benchmarks multiplying an unsigned 256-bit big-endian -// integer by small integers with [FieldVal.MulInt]. -func BenchmarkFieldMulInt(b *testing.B) { - fHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal(fHex) +// BenchmarkElementMulInt benchmarks multiplying an unsigned 256-bit big-endian +// integer by small integers with [Element.MulInt]. +func BenchmarkElementMulInt(b *testing.B) { + valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(valHex) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - f.MulInt(2) + e.MulInt(2) } } @@ -169,19 +169,19 @@ func BenchmarkBigIntMulModP(b *testing.B) { } } -// BenchmarkFieldMul benchmarks multiplying two unsigned 256-bit big-endian -// integers modulo the field prime with [FieldVal]. -func BenchmarkFieldMul(b *testing.B) { +// BenchmarkElementMul benchmarks multiplying two unsigned 256-bit big-endian +// integers modulo the field prime with [Element]. +func BenchmarkElementMul(b *testing.B) { // The function is constant time so any values are fine. f1Hex := "d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab" f2Hex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f1 := mustFieldVal(f1Hex) - f2 := mustFieldVal(f2Hex) + f1 := mustElement(f1Hex) + f2 := mustElement(f2Hex) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - var prod FieldVal + var prod Element prod.Mul2(f1, f2) } } @@ -199,18 +199,18 @@ func BenchmarkBigIntSqrtModP(b *testing.B) { } } -// BenchmarkFieldSqrt benchmarks calculating the square root of an unsigned -// 256-bit big-endian integer modulo the field prime with [FieldVal]. -func BenchmarkFieldSqrt(b *testing.B) { +// BenchmarkElementSqrt benchmarks calculating the square root of an unsigned +// 256-bit big-endian integer modulo the field prime with [Element]. +func BenchmarkElementSqrt(b *testing.B) { // The function is constant time so any value is fine. valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal(valHex) + e := mustElement(valHex) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - var result FieldVal - _ = result.SquareRootVal(f) + var result Element + _ = result.SquareRootVal(e) } } @@ -228,18 +228,18 @@ func BenchmarkBigIntSquareModP(b *testing.B) { } } -// BenchmarkFieldSquare benchmarks squaring a 256-bit big-endian integer modulo -// the field prime with [FieldVal]. -func BenchmarkFieldSquare(b *testing.B) { +// BenchmarkElementSquare benchmarks squaring a 256-bit big-endian integer +// modulo the field prime with [Element]. +func BenchmarkElementSquare(b *testing.B) { // The function is constant time so any values are fine. - fHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal(fHex) + valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(valHex) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - var sq FieldVal - sq.SquareVal(f) + var sq Element + sq.SquareVal(e) } } @@ -257,17 +257,17 @@ func BenchmarkBigIntInverseModP(b *testing.B) { } } -// BenchmarkFieldInverse calculating the multiplicative inverse of an unsigned -// 256-bit big-endian integer modulo the field prime with [FieldVal]. -func BenchmarkFieldInverse(b *testing.B) { +// BenchmarkElementInverse calculating the multiplicative inverse of an unsigned +// 256-bit big-endian integer modulo the field prime with [Element]. +func BenchmarkElementInverse(b *testing.B) { // The function is constant time so any value is fine. valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal(valHex) + e := mustElement(valHex) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - f.Inverse() + e.Inverse() } } @@ -290,17 +290,17 @@ func BenchmarkBigIntIsGtOrEqPrimeMinusOrder(b *testing.B) { } } -// BenchmarkFieldIsGtOrEqPrimeMinusOrder benchmarks determining whether a value +// BenchmarkElementIsGtOrEqPrimeMinusOrder benchmarks determining whether a value // is greater than or equal to the field prime minus the group order with the // specialized type. -func BenchmarkFieldIsGtOrEqPrimeMinusOrder(b *testing.B) { +func BenchmarkElementIsGtOrEqPrimeMinusOrder(b *testing.B) { // The function is constant time so any value is fine. valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal(valHex) + e := mustElement(valHex) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - _ = f.IsGtOrEqPrimeMinusOrder() + _ = e.IsGtOrEqPrimeMinusOrder() } } diff --git a/dcrec/secp256k1/field10x26/element_test.go b/dcrec/secp256k1/field10x26/element_test.go index a38fe0df7a..dafd95ae5b 100644 --- a/dcrec/secp256k1/field10x26/element_test.go +++ b/dcrec/secp256k1/field10x26/element_test.go @@ -16,9 +16,9 @@ import ( "time" ) -// randIntAndFieldVal returns a big integer and a field value both created from -// the same random value generated by the passed rng. -func randIntAndFieldVal(t *testing.T, rng *rand.Rand) (*big.Int, *FieldVal) { +// randIntAndElement returns a [big.Int] and [Element] both created from the +// same random value generated by the passed rng. +func randIntAndElement(t *testing.T, rng *rand.Rand) (*big.Int, *Element) { t.Helper() var buf [32]byte @@ -26,17 +26,17 @@ func randIntAndFieldVal(t *testing.T, rng *rand.Rand) (*big.Int, *FieldVal) { t.Fatalf("failed to read random: %v", err) } - // Create and return both a big integer and a field value. + // Create and return both a big integer and a field element. bigIntVal := new(big.Int).SetBytes(buf[:]) bigIntVal.Mod(bigIntVal, curveParams.P) - var fv FieldVal + var fv Element fv.SetBytes(&buf) return bigIntVal, &fv } -// TestFieldSetInt ensures that setting a field value to various native -// integers works as expected. -func TestFieldSetInt(t *testing.T) { +// TestElementSetInt ensures that setting an element to various native integers +// works as expected. +func TestElementSetInt(t *testing.T) { tests := []struct { name string // test description in uint16 // test value @@ -56,19 +56,19 @@ func TestFieldSetInt(t *testing.T) { }} for _, test := range tests { - f := new(FieldVal).SetInt(test.in) - if !reflect.DeepEqual(f.n, test.expected) { - t.Errorf("%s: wrong result\ngot: %v\nwant: %v", test.name, f.n, + e := new(Element).SetInt(test.in) + if !reflect.DeepEqual(e.n, test.expected) { + t.Errorf("%s: wrong result\ngot: %v\nwant: %v", test.name, e.n, test.expected) continue } } } -// TestFieldSetBytes ensures that setting a field value to a 256-bit big-endian +// TestElementSetBytes ensures that setting an element to a 256-bit big-endian // unsigned integer via both the slice and array methods works as expected for // edge cases. Random cases are tested via the various other tests. -func TestFieldSetBytes(t *testing.T) { +func TestElementSetBytes(t *testing.T) { tests := []struct { name string // test description in string // hex encoded test value @@ -197,7 +197,7 @@ func TestFieldSetBytes(t *testing.T) { inBytes := hexToBytes(test.in) // Ensure setting the bytes via the slice method works as expected. - var f FieldVal + var f Element overflow := f.SetByteSlice(inBytes) if !reflect.DeepEqual(f.n, test.expected) { t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, f.n, @@ -214,7 +214,7 @@ func TestFieldSetBytes(t *testing.T) { } // Ensure setting the bytes via the array method works as expected. - var f2 FieldVal + var f2 Element var b32 [32]byte truncatedInBytes := inBytes if len(truncatedInBytes) > 32 { @@ -238,10 +238,10 @@ func TestFieldSetBytes(t *testing.T) { } } -// TestFieldBytes ensures that retrieving the bytes for a 256-bit big-endian +// TestElementBytes ensures that retrieving the bytes for a 256-bit big-endian // unsigned integer via the various methods works as expected for edge cases. // Random cases are tested via the various other tests. -func TestFieldBytes(t *testing.T) { +func TestElementBytes(t *testing.T) { tests := []struct { name string // test description in string // hex encoded test value @@ -305,11 +305,11 @@ func TestFieldBytes(t *testing.T) { }} for _, test := range tests { - f := mustFieldValWithOverflow(test.in).Normalize() + e := mustElementWithOverflow(test.in).Normalize() expected := hexToBytes(test.expected) // Ensure getting the bytes works as expected. - gotBytes := f.Bytes() + gotBytes := e.Bytes() if !bytes.Equal(gotBytes[:], expected) { t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, *gotBytes, expected) @@ -318,7 +318,7 @@ func TestFieldBytes(t *testing.T) { // Ensure getting the bytes directly into an array works as expected. var b32 [32]byte - f.PutBytes(&b32) + e.PutBytes(&b32) if !bytes.Equal(b32[:], expected) { t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, b32, expected) @@ -327,7 +327,7 @@ func TestFieldBytes(t *testing.T) { // Ensure getting the bytes directly into a slice works as expected. var buffer [64]byte - f.PutBytesUnchecked(buffer[:]) + e.PutBytesUnchecked(buffer[:]) if !bytes.Equal(buffer[:32], expected) { t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, buffer[:32], expected) @@ -336,11 +336,12 @@ func TestFieldBytes(t *testing.T) { } } -// TestFieldZero ensures that zeroing a field value works as expected. -func TestFieldZero(t *testing.T) { - f := mustFieldVal("a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5") - f.Zero() - for idx, rawInt := range f.n { +// TestElementZero ensures that zeroing an element via [Element.Zero] works as +// expected. +func TestElementZero(t *testing.T) { + e := mustElement("a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5") + e.Zero() + for idx, rawInt := range e.n { if rawInt != 0 { t.Errorf("internal integer at index #%d is not zero - got %d", idx, rawInt) @@ -348,46 +349,46 @@ func TestFieldZero(t *testing.T) { } } -// TestFieldIsZero ensures that checking if a field is zero via -// [FieldVal.IsZero] and [FieldVal.IsZeroBit] works as expected. -func TestFieldIsZero(t *testing.T) { - f := new(FieldVal) - if !f.IsZero() { - t.Errorf("new field value is not zero - got %v (rawints %x)", f, f.n) +// TestElementIsZero ensures that checking if an element is zero via +// [Element.IsZero] and [Element.IsZeroBit] works as expected. +func TestElementIsZero(t *testing.T) { + e := new(Element) + if !e.IsZero() { + t.Errorf("new element is not zero - got %v (rawints %x)", e, e.n) } - if f.IsZeroBit() != 1 { - t.Errorf("new field value is not zero - got %v (rawints %x)", f, f.n) + if e.IsZeroBit() != 1 { + t.Errorf("new element is not zero - got %v (rawints %x)", e, e.n) } - f.SetInt(1) - if f.IsZero() { - t.Errorf("claims zero for nonzero field - got %v (rawints %x)", f, f.n) + e.SetInt(1) + if e.IsZero() { + t.Errorf("claims zero for nonzero element- got %v (rawints %x)", e, e.n) } - if f.IsZeroBit() == 1 { - t.Errorf("claims zero for nonzero field - got %v (rawints %x)", f, f.n) + if e.IsZeroBit() == 1 { + t.Errorf("claims zero for nonzero element - got %v (rawints %x)", e, e.n) } - f.Zero() - if !f.IsZero() { - t.Errorf("claims nonzero for zero field - got %v (rawints %x)", f, f.n) + e.Zero() + if !e.IsZero() { + t.Errorf("claims nonzero for zero element - got %v (rawints %x)", e, e.n) } - if f.IsZeroBit() != 1 { - t.Errorf("claims nonzero for zero field - got %v (rawints %x)", f, f.n) + if e.IsZeroBit() != 1 { + t.Errorf("claims nonzero for zero element - got %v (rawints %x)", e, e.n) } - f.SetInt(1) - f.Zero() - if !f.IsZero() { - t.Errorf("claims zero for nonzero field - got %v (rawints %x)", f, f.n) + e.SetInt(1) + e.Zero() + if !e.IsZero() { + t.Errorf("claims zero for nonzero element - got %v (rawints %x)", e, e.n) } - if f.IsZeroBit() != 1 { - t.Errorf("claims zero for nonzero field - got %v (rawints %x)", f, f.n) + if e.IsZeroBit() != 1 { + t.Errorf("claims zero for nonzero element - got %v (rawints %x)", e, e.n) } } -// TestFieldIsOne ensures that checking if a field is one via [FieldVal.IsOne] -// and [FieldVal.IsOneBit] works as expected. -func TestFieldIsOne(t *testing.T) { +// TestElementIsOne ensures that checking if an element is one via +// [Element.IsOne] and [Element.IsOneBit] works as expected. +func TestElementIsOne(t *testing.T) { tests := []struct { name string // test description in string // hex encoded test value @@ -424,65 +425,65 @@ func TestFieldIsOne(t *testing.T) { normalize: false, expected: false, }, { - name: "2^26 (one bit in second internal field word", + name: "2^26 (one bit in second internal limb)", in: "4000000", normalize: false, expected: false, }, { - name: "2^52 (one bit in third internal field word", + name: "2^52 (one bit in third internal limb)", in: "10000000000000", normalize: false, expected: false, }, { - name: "2^78 (one bit in fourth internal field word", + name: "2^78 (one bit in fourth internal limb)", in: "40000000000000000000", normalize: false, expected: false, }, { - name: "2^104 (one bit in fifth internal field word", + name: "2^104 (one bit in fifth internal limb)", in: "100000000000000000000000000", normalize: false, expected: false, }, { - name: "2^130 (one bit in sixth internal field word", + name: "2^130 (one bit in sixth internal limb)", in: "400000000000000000000000000000000", normalize: false, expected: false, }, { - name: "2^156 (one bit in seventh internal field word", + name: "2^156 (one bit in seventh internal limb)", in: "1000000000000000000000000000000000000000", normalize: false, expected: false, }, { - name: "2^182 (one bit in eighth internal field word", + name: "2^182 (one bit in eighth internal limb)", in: "4000000000000000000000000000000000000000000000", normalize: false, expected: false, }, { - name: "2^208 (one bit in ninth internal field word", + name: "2^208 (one bit in ninth internal limb)", in: "10000000000000000000000000000000000000000000000000000", normalize: false, expected: false, }, { - name: "2^234 (one bit in tenth internal field word", + name: "2^234 (one bit in tenth internal limb)", in: "40000000000000000000000000000000000000000000000000000000000", normalize: false, expected: false, }} for _, test := range tests { - f := mustFieldValWithOverflow(test.in) + e := mustElementWithOverflow(test.in) if test.normalize { - f.Normalize() + e.Normalize() } - result := f.IsOne() + result := e.IsOne() if result != test.expected { t.Errorf("%s: wrong result -- got: %v, want: %v", test.name, result, test.expected) continue } - result2 := f.IsOneBit() == 1 + result2 := e.IsOneBit() == 1 if result2 != test.expected { t.Errorf("%s: wrong result -- got: %v, want: %v", test.name, result2, test.expected) @@ -491,8 +492,8 @@ func TestFieldIsOne(t *testing.T) { } } -// TestFieldStringer ensures the stringer returns the appropriate hex string. -func TestFieldStringer(t *testing.T) { +// TestElementStringer ensures the stringer returns the appropriate hex string. +func TestElementStringer(t *testing.T) { tests := []struct { name string // test description in string // hex encoded test value @@ -576,8 +577,8 @@ func TestFieldStringer(t *testing.T) { }} for _, test := range tests { - f := mustFieldValWithOverflow(test.in) - result := f.String() + e := mustElementWithOverflow(test.in) + result := e.String() if result != test.expected { t.Errorf("%s: wrong result\ngot: %v\nwant: %v", test.name, result, test.expected) @@ -586,9 +587,9 @@ func TestFieldStringer(t *testing.T) { } } -// TestFieldNormalize ensures that normalizing the internal field words works as +// TestElementNormalize ensures that normalizing the internal limbs works as // expected. -func TestFieldNormalize(t *testing.T) { +func TestElementNormalize(t *testing.T) { tests := []struct { name string // test description raw [10]uint32 // Intentionally denormalized value @@ -666,8 +667,8 @@ func TestFieldNormalize(t *testing.T) { raw: [10]uint32{0xffffffff, 0xffffffc0, 0xffffffc0, 0xffffffc0, 0xffffffc0, 0xffffffc0, 0xffffffc0, 0xffffffc0, 0xffffffc0, 0x3fffc0}, normalized: [10]uint32{0x000003d0, 0x00000040, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x000000}, }, { - // Prime with field representation such that the initial reduction does - // not result in a carry to bit 256. + // Prime with internal representation such that the initial reduction + // does not result in a carry to bit 256. // // 2^256 - 4294968273 (secp256k1 prime) name: "2^256 - 4294968273 (secp256k1 prime)", @@ -696,7 +697,7 @@ func TestFieldNormalize(t *testing.T) { // than P when it has a magnitude of 1 due to a carry to bit 256, but // would not be without the carry. These values come from the fact that // P is 2^256 - 4294968273 and 977 is the low order word in the internal - // field representation. + // representation. // // 2^256 * 5 - ((4294968273 - (977+1)) * 4) name: "2^256 * 5 - ((4294968273 - (977+1)) * 4)", @@ -793,20 +794,20 @@ func TestFieldNormalize(t *testing.T) { }} for _, test := range tests { - f := new(FieldVal) - f.n = test.raw - f.Normalize() - if !reflect.DeepEqual(f.n, test.normalized) { + e := new(Element) + e.n = test.raw + e.Normalize() + if !reflect.DeepEqual(e.n, test.normalized) { t.Errorf("%s: wrong normalized result\ngot: %x\nwant: %x", - test.name, f.n, test.normalized) + test.name, e.n, test.normalized) continue } } } -// TestFieldIsOdd ensures that checking if a field value is odd via -// [FieldVal.IsOdd] and [FieldVal.IsOddBit] works as expected. -func TestFieldIsOdd(t *testing.T) { +// TestElementIsOdd ensures that checking if an element is odd via +// [Element.IsOdd] and [Element.IsOddBit] works as expected. +func TestElementIsOdd(t *testing.T) { tests := []struct { name string // test description in string // hex encoded value @@ -842,15 +843,15 @@ func TestFieldIsOdd(t *testing.T) { }} for _, test := range tests { - f := mustFieldValWithOverflow(test.in) - result := f.IsOdd() + e := mustElementWithOverflow(test.in) + result := e.IsOdd() if result != test.expected { t.Errorf("%s: wrong result -- got: %v, want: %v", test.name, result, test.expected) continue } - result2 := f.IsOddBit() == 1 + result2 := e.IsOddBit() == 1 if result2 != test.expected { t.Errorf("%s: wrong result -- got: %v, want: %v", test.name, result2, test.expected) @@ -859,9 +860,9 @@ func TestFieldIsOdd(t *testing.T) { } } -// TestFieldEquals ensures that checking two field values for equality via -// [FieldVal.Equals] works as expected. -func TestFieldEquals(t *testing.T) { +// TestElementEquals ensures that checking two elements for equality via +// [Element.Equals] works as expected. +func TestElementEquals(t *testing.T) { tests := []struct { name string // test description in1 string // hex encoded value @@ -905,9 +906,9 @@ func TestFieldEquals(t *testing.T) { }} for _, test := range tests { - f := mustFieldValWithOverflow(test.in1).Normalize() - f2 := mustFieldValWithOverflow(test.in2).Normalize() - result := f.Equals(f2) + e := mustElementWithOverflow(test.in1).Normalize() + e2 := mustElementWithOverflow(test.in2).Normalize() + result := e.Equals(e2) if result != test.expected { t.Errorf("%s: wrong result -- got: %v, want: %v", test.name, result, test.expected) @@ -916,9 +917,9 @@ func TestFieldEquals(t *testing.T) { } } -// TestFieldNegate ensures that negating field values via [FieldVal.Negate] -// works as expected. -func TestFieldNegate(t *testing.T) { +// TestElementNegate ensures that negating elements via [Element.Negate] works +// as expected. +func TestElementNegate(t *testing.T) { tests := []struct { name string // test description in string // hex encoded test value @@ -966,11 +967,11 @@ func TestFieldNegate(t *testing.T) { }} for _, test := range tests { - f := mustFieldValWithOverflow(test.in).Normalize() - expected := mustFieldVal(test.expected).Normalize() + e := mustElementWithOverflow(test.in).Normalize() + expected := mustElement(test.expected).Normalize() - // Ensure negating another value produces the expected result. - result := new(FieldVal).NegateVal(f, 1).Normalize() + // Ensure negating another element produces the expected result. + result := new(Element).NegateVal(e, 1).Normalize() if !result.Equals(expected) { t.Errorf("%s: unexpected result -- got: %v, want: %v", test.name, result, expected) @@ -978,7 +979,7 @@ func TestFieldNegate(t *testing.T) { } // Ensure self negating also produces the expected result. - result2 := f.Negate(1).Normalize() + result2 := e.Negate(1).Normalize() if !result2.Equals(expected) { t.Errorf("%s: unexpected result -- got: %v, want: %v", test.name, result2, expected) @@ -987,9 +988,9 @@ func TestFieldNegate(t *testing.T) { } } -// TestFieldAddInt ensures that adding an integer to field values via -// [FieldVal.AddInt] works as expected. -func TestFieldAddInt(t *testing.T) { +// TestElementAddInt ensures that adding an integer to elements via +// [Element.AddInt] works as expected. +func TestElementAddInt(t *testing.T) { tests := []struct { name string // test description in1 string // hex encoded value @@ -1043,9 +1044,9 @@ func TestFieldAddInt(t *testing.T) { }} for _, test := range tests { - f := mustFieldValWithOverflow(test.in1).Normalize() - expected := mustFieldVal(test.expected).Normalize() - result := f.AddInt(test.in2).Normalize() + e := mustElementWithOverflow(test.in1).Normalize() + expected := mustElement(test.expected).Normalize() + result := e.AddInt(test.in2).Normalize() if !result.Equals(expected) { t.Errorf("%s: wrong result -- got: %v -- want: %v", test.name, result, expected) @@ -1054,9 +1055,11 @@ func TestFieldAddInt(t *testing.T) { } } -// TestFieldAdd ensures that adding two field values together via [FieldVal.Add] -// and [FieldVal.Add2] works as expected. -func TestFieldAdd(t *testing.T) { +// TestElementAdd ensures that adding two elements together via [Element.Add] +// and [Element.Add2] works as expected. +// +// nolint: dupl +func TestElementAdd(t *testing.T) { tests := []struct { name string // test description in1 string // first hex encoded value @@ -1126,21 +1129,21 @@ func TestFieldAdd(t *testing.T) { for _, test := range tests { // Parse test hex. - f1 := mustFieldValWithOverflow(test.in1).Normalize() - f2 := mustFieldValWithOverflow(test.in2).Normalize() - expected := mustFieldVal(test.expected).Normalize() + f1 := mustElementWithOverflow(test.in1).Normalize() + f2 := mustElementWithOverflow(test.in2).Normalize() + expected := mustElement(test.expected).Normalize() // Ensure adding the two values with the result going to another // variable produces the expected result. - result := new(FieldVal).Add2(f1, f2).Normalize() + result := new(Element).Add2(f1, f2).Normalize() if !result.Equals(expected) { t.Errorf("%s: unexpected result\ngot: %v\nwant: %v", test.name, result, expected) continue } - // Ensure adding the value to an existing field value produces the - // expected result. + // Ensure adding the value to an existing element produces the expected + // result. f1.Add(f2).Normalize() if !f1.Equals(expected) { t.Errorf("%s: unexpected result\ngot: %v\nwant: %v", test.name, @@ -1150,18 +1153,18 @@ func TestFieldAdd(t *testing.T) { } } -// TestFieldMulByX ensures [FieldVal.MulBy2], [FieldVal.MulBy3], -// [FieldVal.MulBy4], and [FieldVal.MulBy8] produce the same results as the -// equivalent [FieldVal.MulInt]. -func TestFieldMulByX(t *testing.T) { +// TestElementMulByX ensures [Element.MulBy2], [Element.MulBy3], +// [Element.MulBy4], and [Element.MulBy8] produce the same results as the +// equivalent [Element.MulInt]. +func TestElementMulByX(t *testing.T) { mulByFuncs := []struct { factor uint8 - fn func(*FieldVal) *FieldVal + fn func(*Element) *Element }{ - {2, (*FieldVal).MulBy2}, - {3, (*FieldVal).MulBy3}, - {4, (*FieldVal).MulBy4}, - {8, (*FieldVal).MulBy8}, + {2, (*Element).MulBy2}, + {3, (*Element).MulBy3}, + {4, (*Element).MulBy4}, + {8, (*Element).MulBy8}, } tests := []struct { @@ -1205,10 +1208,10 @@ func TestFieldMulByX(t *testing.T) { in: "e3cbe002cc93029190181a906ed41af401c9726546dc19389a06290efdf563f1", }} for _, test := range tests { - in := mustFieldVal(test.in) + in := mustElement(test.in) for _, m := range mulByFuncs { - want := new(FieldVal).Set(in).MulInt(m.factor) - got := m.fn(new(FieldVal).Set(in)) + want := new(Element).Set(in).MulInt(m.factor) + got := m.fn(new(Element).Set(in)) if !got.Equals(want) { t.Errorf("%q: MulBy%d: wrong result -- got: %v, want: %v", test.name, m.factor, got, want) @@ -1217,9 +1220,9 @@ func TestFieldMulByX(t *testing.T) { } } -// TestFieldMulInt ensures that multiplying an integer to field values via -// [FieldVal.MulInt] works as expected. -func TestFieldMulInt(t *testing.T) { +// TestElementMulInt ensures that multiplying an integer to elements via +// [Element.MulInt] works as expected. +func TestElementMulInt(t *testing.T) { tests := []struct { name string // test description in1 string // hex encoded value @@ -1286,9 +1289,9 @@ func TestFieldMulInt(t *testing.T) { }} for _, test := range tests { - f := mustFieldValWithOverflow(test.in1).Normalize() - expected := mustFieldVal(test.expected).Normalize() - result := f.MulInt(test.in2).Normalize() + e := mustElementWithOverflow(test.in1).Normalize() + expected := mustElement(test.expected).Normalize() + result := e.MulInt(test.in2).Normalize() if !result.Equals(expected) { t.Errorf("%s: wrong result -- got: %v -- want: %v", test.name, result, expected) @@ -1297,9 +1300,11 @@ func TestFieldMulInt(t *testing.T) { } } -// TestFieldMul ensures that multiplying two field values via [FieldVal.Mul] and -// [FieldVal.Mul2] works as expected. -func TestFieldMul(t *testing.T) { +// TestElementMul ensures that multiplying two elements via [Element.Mul] and +// [Element.Mul2] works as expected. +// +// nolint: dupl +func TestElementMul(t *testing.T) { tests := []struct { name string // test description in1 string // first hex encoded value @@ -1368,20 +1373,20 @@ func TestFieldMul(t *testing.T) { }} for _, test := range tests { - f1 := mustFieldValWithOverflow(test.in1).Normalize() - f2 := mustFieldValWithOverflow(test.in2).Normalize() - expected := mustFieldVal(test.expected).Normalize() + f1 := mustElementWithOverflow(test.in1).Normalize() + f2 := mustElementWithOverflow(test.in2).Normalize() + expected := mustElement(test.expected).Normalize() // Ensure multiplying the two values with the result going to another // variable produces the expected result. - result := new(FieldVal).Mul2(f1, f2).Normalize() + result := new(Element).Mul2(f1, f2).Normalize() if !result.Equals(expected) { t.Errorf("%s: unexpected result\ngot: %v\nwant: %v", test.name, result, expected) continue } - // Ensure multiplying the value to an existing field value produces the + // Ensure multiplying the value to an existing element produces the // expected result. f1.Mul(f2).Normalize() if !f1.Equals(expected) { @@ -1392,9 +1397,9 @@ func TestFieldMul(t *testing.T) { } } -// TestFieldSquare ensures that squaring field values via [FieldVal.Square] and -// [FieldVal.SqualVal] works as expected. -func TestFieldSquare(t *testing.T) { +// TestElementSquare ensures that squaring elements via [Element.Square] and +// [Element.SqualVal] works as expected. +func TestElementSquare(t *testing.T) { tests := []struct { name string // test description in string // hex encoded value @@ -1434,32 +1439,32 @@ func TestFieldSquare(t *testing.T) { }} for _, test := range tests { - f := mustFieldValWithOverflow(test.in).Normalize() - expected := mustFieldVal(test.expected).Normalize() + e := mustElementWithOverflow(test.in).Normalize() + expected := mustElement(test.expected).Normalize() // Ensure squaring the value with the result going to another variable // produces the expected result. - result := new(FieldVal).SquareVal(f).Normalize() + result := new(Element).SquareVal(e).Normalize() if !result.Equals(expected) { t.Errorf("%s: unexpected result\ngot: %v\nwant: %v", test.name, result, expected) continue } - // Ensure self squaring an existing field value produces the expected + // Ensure self squaring an existing element produces the expected // result. - f.Square().Normalize() - if !f.Equals(expected) { + e.Square().Normalize() + if !e.Equals(expected) { t.Errorf("%s: unexpected result\ngot: %v\nwant: %v", test.name, - f, expected) + e, expected) continue } } } -// TestFieldSquareRoot ensures that calculating the square root of field values -// via [FieldVal.SquareRootVal] works as expected for edge cases. -func TestFieldSquareRoot(t *testing.T) { +// TestElementSquareRoot ensures that calculating the square root of elements +// via [Element.SquareRootVal] works as expected for edge cases. +func TestElementSquareRoot(t *testing.T) { tests := []struct { name string // test description in string // hex encoded value @@ -1528,12 +1533,12 @@ func TestFieldSquareRoot(t *testing.T) { }} for _, test := range tests { - input := mustFieldValWithOverflow(test.in).Normalize() - want := mustFieldVal(test.want).Normalize() + input := mustElementWithOverflow(test.in).Normalize() + want := mustElement(test.want).Normalize() // Calculate the square root and enusre the validity flag matches the // expected value. - var result FieldVal + var result Element isValid := result.SquareRootVal(input) if isValid != test.valid { t.Errorf("%s: mismatched validity -- got %v, want %v", test.name, @@ -1551,10 +1556,10 @@ func TestFieldSquareRoot(t *testing.T) { } } -// TestFieldSquareRootRandom ensures that calculating the square root for random -// field values works as expected by also performing the same operation with big -// ints and comparing the results. -func TestFieldSquareRootRandom(t *testing.T) { +// TestElementSquareRootRandom ensures that calculating the square root for +// random elements works as expected by also performing the same operation with +// big ints and comparing the results. +func TestElementSquareRootRandom(t *testing.T) { // Use a unique random seed each test instance and log it if the tests fail. seed := time.Now().Unix() rng := rand.New(rand.NewSource(seed)) @@ -1565,39 +1570,39 @@ func TestFieldSquareRootRandom(t *testing.T) { }(t, seed) for i := 0; i < 100; i++ { - // Generate big integer and field value with the same random value. - bigIntVal, fVal := randIntAndFieldVal(t, rng) + // Generate big integer and element with the same random value. + bigIntVal, elem := randIntAndElement(t, rng) // Calculate the square root of the value using big ints. bigIntResult := new(big.Int).ModSqrt(bigIntVal, curveParams.P) bigIntHasSqrt := bigIntResult != nil - // Calculate the square root of the value using a field value. - var fValResult FieldVal - fValHasSqrt := fValResult.SquareRootVal(fVal) + // Calculate the square root of the value using an element. + var elemResult Element + elemHasSqrt := elemResult.SquareRootVal(elem) // Ensure they match. - if bigIntHasSqrt != fValHasSqrt { - t.Fatalf("mismatched square root existence\nbig int in: %x\nfield "+ - "in: %v\nbig int result: %v\nfield result %v", bigIntVal, fVal, - bigIntHasSqrt, fValHasSqrt) + if bigIntHasSqrt != elemHasSqrt { + t.Fatalf("mismatched square root existence\nbig int in: %x\nelem "+ + "in: %v\nbig int result: %v\nelem result %v", bigIntVal, elem, + bigIntHasSqrt, elemHasSqrt) } - if !fValHasSqrt { + if !elemHasSqrt { continue } bigIntResultHex := fmt.Sprintf("%064x", bigIntResult) - fieldValResultHex := fmt.Sprintf("%v", fValResult) - if bigIntResultHex != fieldValResultHex { - t.Fatalf("mismatched square root\nbig int in: %x\nfield in: %v\n"+ - "big int result: %x\nfield result %v", bigIntVal, fVal, - bigIntResult, fValResult) + elemResultHex := fmt.Sprintf("%v", elemResult) + if bigIntResultHex != elemResultHex { + t.Fatalf("mismatched square root\nbig int in: %x\nelem in: %v\n"+ + "big int result: %x\nelem result %v", bigIntVal, elem, + bigIntResult, elemResult) } } } -// TestFieldInverse ensures that finding the multiplicative inverse via -// [FieldVal.Inverse] works as expected. -func TestFieldInverse(t *testing.T) { +// TestElementInverse ensures that finding the multiplicative inverse via +// [Element.Inverse] works as expected. +func TestElementInverse(t *testing.T) { tests := []struct { name string // test description in string // hex encoded value @@ -1637,9 +1642,9 @@ func TestFieldInverse(t *testing.T) { }} for _, test := range tests { - f := mustFieldValWithOverflow(test.in).Normalize() - expected := mustFieldVal(test.expected).Normalize() - result := f.Inverse().Normalize() + e := mustElementWithOverflow(test.in).Normalize() + expected := mustElement(test.expected).Normalize() + result := e.Inverse().Normalize() if !result.Equals(expected) { t.Errorf("%s: d wrong result\ngot: %v\nwant: %v", test.name, result, expected) @@ -1648,10 +1653,10 @@ func TestFieldInverse(t *testing.T) { } } -// TestFieldIsGtOrEqPrimeMinusOrder ensures that field values report whether or +// TestElementIsGtOrEqPrimeMinusOrder ensures that elements report whether or // not they are greater than or equal to the field prime minus the group order // as expected for edge cases. -func TestFieldIsGtOrEqPrimeMinusOrder(t *testing.T) { +func TestElementIsGtOrEqPrimeMinusOrder(t *testing.T) { tests := []struct { name string // test description in string // hex encoded test value @@ -1715,7 +1720,7 @@ func TestFieldIsGtOrEqPrimeMinusOrder(t *testing.T) { }} for _, test := range tests { - result := mustFieldVal(test.in).IsGtOrEqPrimeMinusOrder() + result := mustElement(test.in).IsGtOrEqPrimeMinusOrder() if result != test.expected { t.Errorf("%s: unexpected result -- got: %v, want: %v", test.name, result, test.expected) @@ -1724,11 +1729,11 @@ func TestFieldIsGtOrEqPrimeMinusOrder(t *testing.T) { } } -// TestFieldIsGtOrEqPrimeMinusOrderRandom ensures that field values report -// whether or not they are greater than or equal to the field prime minus the -// group order as expected by also performing the same operation with big ints -// and comparing the results. -func TestFieldIsGtOrEqPrimeMinusOrderRandom(t *testing.T) { +// TestElementIsGtOrEqPrimeMinusOrderRandom ensures that elements report whether +// or not they are greater than or equal to the field prime minus the group +// order as expected by also performing the same operation with big ints and +// comparing the results. +func TestElementIsGtOrEqPrimeMinusOrderRandom(t *testing.T) { // Use a unique random seed each test instance and log it if the tests fail. seed := time.Now().Unix() rng := rand.New(rand.NewSource(seed)) @@ -1740,22 +1745,22 @@ func TestFieldIsGtOrEqPrimeMinusOrderRandom(t *testing.T) { bigPMinusN := new(big.Int).Sub(curveParams.P, curveParams.N) for i := 0; i < 100; i++ { - // Generate big integer and field value with the same random value. - bigIntVal, fVal := randIntAndFieldVal(t, rng) + // Generate big integer and element with the same random value. + bigIntVal, elem := randIntAndElement(t, rng) // Determine the value is greater than or equal to the prime minus the // order using big ints. bigIntResult := bigIntVal.Cmp(bigPMinusN) >= 0 // Determine the value is greater than or equal to the prime minus the - // order using a field value. - fValResult := fVal.IsGtOrEqPrimeMinusOrder() + // order using an element. + fValResult := elem.IsGtOrEqPrimeMinusOrder() // Ensure they match. if bigIntResult != fValResult { t.Fatalf("mismatched is gt or eq prime minus order\nbig int in: "+ - "%x\nscalar in: %v\nbig int result: %v\nscalar result %v", - bigIntVal, fVal, bigIntResult, fValResult) + "%x\nelement in: %v\nbig int result: %v\nelement result %v", + bigIntVal, elem, bigIntResult, fValResult) } } } From 50b4acdb076ec47b2d222dd0e47888e2766acad0 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 29 Jul 2026 01:09:46 -0500 Subject: [PATCH 28/33] secp256k1: Wrap public field implementation. This replaces the public FieldVal type alias with a wrapper around the underlying field implementation. This preserves the existing API while allowing the concrete field implementation to vary independently of the public type. It also ensures the public documentation consistently describes the most restrictive semantics required by all supported implementations rather than those of whichever implementation happens to be selected. The wrapper forwards all methods directly to the underlying implementation, so no functional changes are intended. This is part of a series of commits that will ultimately allow architecture-specific backend selection. --- dcrec/secp256k1/curve.go | 4 - dcrec/secp256k1/fieldwrap.go | 584 +++++++++++++++++++++++++++++++++++ 2 files changed, 584 insertions(+), 4 deletions(-) create mode 100644 dcrec/secp256k1/fieldwrap.go diff --git a/dcrec/secp256k1/curve.go b/dcrec/secp256k1/curve.go index e6c6d4f629..fb9da3d265 100644 --- a/dcrec/secp256k1/curve.go +++ b/dcrec/secp256k1/curve.go @@ -8,8 +8,6 @@ package secp256k1 import ( "encoding/hex" "math/bits" - - "github.com/decred/dcrd/dcrec/secp256k1/v4/field10x26" ) // References: @@ -28,8 +26,6 @@ import ( // (x, y) position on the curve, the Jacobian coordinates are (x1, y1, z1) // where x = x1/z1^2 and y = y1/z1^3. -type FieldVal = field10x26.Element - // mustFieldValInternal converts the passed hex string into a [FieldVal] and // will panic if there is an error. Values that overflow are treated as an // error unless the allow overflow flag is set. diff --git a/dcrec/secp256k1/fieldwrap.go b/dcrec/secp256k1/fieldwrap.go new file mode 100644 index 0000000000..dd46aa8f1c --- /dev/null +++ b/dcrec/secp256k1/fieldwrap.go @@ -0,0 +1,584 @@ +// Copyright (c) 2015-2026 The Decred developers +// Copyright (c) 2013-2026 Dave Collins +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package secp256k1 + +import ( + "github.com/decred/dcrd/dcrec/secp256k1/v4/field10x26" +) + +// ---------------------------------------------------------------------------- +// NOTE: [FieldVal] is intentionally a wrapper as opposed to a direct type +// alias despite it creating some boilerplate. +// +// Some field implementations have more restrictive semantics, so the publicly +// available type must adhere to the most restrictive among all supported ones. +// +// Using a type alias would end up showing the documentation for the +// architecture-specific implementation that is ultimately selected which would +// very likely lead to misleading documentation and incorrect usage. +// ---------------------------------------------------------------------------- + +// fieldImpl defines the concrete finite field implementation. This will +// ultimately allow selection of different concrete implementations. +type fieldImpl = field10x26.Element + +// FieldVal implements optimized fixed-precision arithmetic over the +// secp256k1 finite field. This means all arithmetic is performed modulo +// +// 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f +// +// WARNING: Since it is so important for the field arithmetic to be extremely +// fast for high performance crypto, this type does not perform any validation +// of documented preconditions where it ordinarily would. As a result, it is +// IMPERATIVE for callers to understand some key concepts that are described +// below and ensure the methods are called with the necessary preconditions that +// each method is documented with. For example, some methods only give the +// correct result if the field element is normalized and others require the +// field elements involved to have a maximum magnitude and THERE ARE NO EXPLICIT +// CHECKS TO ENSURE THOSE PRECONDITIONS ARE SATISFIED. This does, +// unfortunately, make the type more difficult to use correctly and while it is +// typically preferable to ensure all state and input is valid for most code, +// this is a bit of an exception because those extra checks really add up in +// what ends up being critical hot paths. +// +// The first key concept when working with this type is normalization. In order +// to avoid the need to propagate a ton of carries, the internal representation +// provides additional overflow bits for each limb of the overall 256-bit value. +// This means that there are multiple internal representations for the same +// value and, as a result, any methods that rely on comparison of the value, +// such as equality and oddness determination, require the caller to provide a +// normalized field element. +// +// The second key concept when working with this type is magnitude. As +// previously mentioned, the internal representation provides additional +// overflow bits which means that the more math operations that are performed on +// the field element between normalizations, the more those overflow bits +// accumulate. The magnitude is effectively that maximum possible number of +// those overflow bits that could possibly be required as a result of a given +// operation. Since there are only a limited number of overflow bits available, +// this implies that the max possible magnitude MUST be tracked by the caller +// and the caller MUST normalize the field element if a given operation would +// cause the magnitude of the result to exceed the max allowed value. +// +// IMPORTANT: The max allowed magnitude of a field element is 32. +type FieldVal struct { + impl fieldImpl +} + +// String returns the field element as a normalized human-readable hex string. +// +// Preconditions: None +// Output Normalized: Field is not modified -- same as input element +// Output Max Magnitude: Field is not modified -- same as input element +func (f FieldVal) String() string { + return f.impl.String() +} + +// Zero sets the field element to zero in constant time. A newly created field +// element is already set to zero. This function can be useful to clear an +// existing field element for reuse. +// +// Preconditions: None +// Output Normalized: Yes +// Output Max Magnitude: 1 +func (f *FieldVal) Zero() { + f.impl.Zero() +} + +// Set sets the field element equal to the passed one in constant time. The +// resulting field element will have the same normalization state and magnitude +// as the passed field element. +// +// The field element is returned to support chaining. This enables syntax like: +// f := new(FieldVal).Set(f2).Add(1) so that f = f2 + 1 where f2 is not +// modified. +// +// Preconditions: None +// Output Normalized: Same as input element +// Output Max Magnitude: Same as input element +func (f *FieldVal) Set(val *FieldVal) *FieldVal { + f.impl.Set(&val.impl) + return f +} + +// SetInt sets the field element to the passed integer in constant time. This +// is a convenience function since it is fairly common to perform arithmetic +// with small native integers. +// +// The field element is returned to support chaining. This enables syntax such +// as f := new(FieldVal).SetInt(2).Mul(f2) so that f = 2 * f2. +// +// Preconditions: None +// Output Normalized: Yes +// Output Max Magnitude: 1 +func (f *FieldVal) SetInt(ui uint16) *FieldVal { + f.impl.SetInt(ui) + return f +} + +// SetBytes packs the passed 32-byte big-endian value into the internal +// representation in constant time. It interprets the provided array as a +// 256-bit big-endian unsigned integer, packs it, and returns either 1 if it is +// greater than or equal to the field prime (aka it overflowed) or 0 otherwise +// in constant time. +// +// Note that a bool is not used here because it is not possible in Go to convert +// from a bool to numeric value in constant time and many constant-time +// operations require a numeric value. +// +// Preconditions: None +// Output Normalized: Yes when no overflow, No otherwise +// Output Max Magnitude: 1 +func (f *FieldVal) SetBytes(b *[32]byte) uint32 { + return f.impl.SetBytes(b) +} + +// SetByteSlice interprets the provided slice as a 256-bit big-endian unsigned +// integer (meaning it is truncated to the first 32 bytes), packs it into the +// internal representation, and returns whether or not the resulting truncated +// 256-bit integer is greater than or equal to the field prime (aka it +// overflowed) in constant time. +// +// Note that since passing a slice with more than 32 bytes is truncated, it is +// possible that the truncated value is less than the field prime and hence it +// will not be reported as having overflowed in that case. It is up to the +// caller to decide whether it needs to provide numbers of the appropriate size +// or it if is acceptable to use this function with the described truncation and +// overflow behavior. +// +// Preconditions: None +// Output Normalized: Yes when no overflow, No otherwise +// Output Max Magnitude: 1 +func (f *FieldVal) SetByteSlice(b []byte) bool { + return f.impl.SetByteSlice(b) +} + +// Normalize converts the internal representation into its canonical +// representation and performs modular reduction over the secp256k1 field prime +// in constant time. +// +// Preconditions: None +// Output Normalized: Yes +// Output Max Magnitude: 1 +func (f *FieldVal) Normalize() *FieldVal { + f.impl.Normalize() + return f +} + +// PutBytesUnchecked unpacks the field element to a 32-byte big-endian value +// directly into the passed byte slice in constant time. The target slice must +// have at least 32 bytes available or it will panic. +// +// There is a similar function, [FieldVal.PutBytes], which unpacks the field +// element into a 32-byte array directly. This version is provided since it can +// be useful to write directly into part of a larger buffer without needing a +// separate allocation. +// +// Preconditions: +// - The field element MUST be normalized +// - The target slice MUST have at least 32 bytes available +func (f *FieldVal) PutBytesUnchecked(b []byte) { + f.impl.PutBytesUnchecked(b) +} + +// PutBytes unpacks the field element to a 32-byte big-endian value using the +// passed byte array in constant time. +// +// There is a similar function, [FieldVal.PutBytesUnchecked], which unpacks the +// field element into a slice that must have at least 32 bytes available. This +// version is provided since it can be useful to write directly into an array +// that is type checked. +// +// Alternatively, there is also [FieldVal.Bytes], which unpacks the field +// element into a new array and returns that which can sometimes be more +// ergonomic in applications that aren't concerned about an additional copy. +// +// Preconditions: +// - The field element MUST be normalized +func (f *FieldVal) PutBytes(b *[32]byte) { + f.impl.PutBytes(b) +} + +// Bytes unpacks the field element to a 32-byte big-endian value in constant +// time. +// +// See [FieldVal.PutBytes] and [FieldVal.PutBytesUnchecked] for variants that +// allow an array or slice to be passed which can be useful to cut down on the +// number of allocations by allowing the caller to reuse a buffer or write +// directly into part of a larger buffer. +// +// Preconditions: +// - The field element MUST be normalized +func (f *FieldVal) Bytes() *[32]byte { + return f.impl.Bytes() +} + +// IsZeroBit returns 1 when the field element is equal to zero or 0 otherwise in +// constant time. +// +// Note that a bool is not used here because it is not possible in Go to convert +// from a bool to numeric value in constant time and many constant-time +// operations require a numeric value. See [FieldVal.IsZero] for the version +// that returns a bool. +// +// Preconditions: +// - The field element MUST be normalized +func (f *FieldVal) IsZeroBit() uint32 { + return f.impl.IsZeroBit() +} + +// IsZero returns whether or not the field element is equal to zero in constant +// time. +// +// Preconditions: +// - The field element MUST be normalized +func (f *FieldVal) IsZero() bool { + return f.impl.IsZero() +} + +// IsOneBit returns 1 when the field element is equal to one or 0 otherwise in +// constant time. +// +// Note that a bool is not used here because it is not possible in Go to convert +// from a bool to numeric value in constant time and many constant-time +// operations require a numeric value. See [FieldVal.IsOne] for the version +// that returns a bool. +// +// Preconditions: +// - The field element MUST be normalized +func (f *FieldVal) IsOneBit() uint32 { + return f.impl.IsOneBit() +} + +// IsOne returns whether or not the field element is equal to one in constant +// time. +// +// Preconditions: +// - The field element MUST be normalized +func (f *FieldVal) IsOne() bool { + return f.impl.IsOne() +} + +// IsOddBit returns 1 when the field element is an odd number or 0 otherwise in +// constant time. +// +// Note that a bool is not used here because it is not possible in Go to convert +// from a bool to numeric value in constant time and many constant-time +// operations require a numeric value. See [FieldVal.IsOdd] for the version +// that returns a bool. +// +// Preconditions: +// - The field element MUST be normalized +func (f *FieldVal) IsOddBit() uint32 { + return f.impl.IsOddBit() +} + +// IsOdd returns whether or not the field element is an odd number in constant +// time. +// +// Preconditions: +// - The field element MUST be normalized +func (f *FieldVal) IsOdd() bool { + return f.impl.IsOdd() +} + +// Equals returns whether or not the two field elements are the same in constant +// time. +// +// Preconditions: +// - Both field elements being compared MUST be normalized +func (f *FieldVal) Equals(val *FieldVal) bool { + return f.impl.Equals(&val.impl) +} + +// NegateVal negates the passed element and stores the result in f in constant +// time. The caller must provide the maximum magnitude of the passed field +// element for a correct result. +// +// The field element is returned to support chaining. This enables syntax like: +// f.NegateVal(f2).AddInt(1) so that f = -f2 + 1. +// +// Preconditions: +// - The max magnitude of the input field element MUST be 31 +// Output Normalized: No +// Output Max Magnitude: Input magnitude + 1 +func (f *FieldVal) NegateVal(val *FieldVal, magnitude uint32) *FieldVal { + f.impl.NegateVal(&val.impl, magnitude) + return f +} + +// Negate negates the field element in constant time. The existing field +// element is modified. The caller must provide the maximum magnitude of the +// field element for a correct result. +// +// The field element is returned to support chaining. This enables syntax like: +// f.Negate().AddInt(1) so that f = -f + 1. +// +// Preconditions: +// - The max magnitude MUST be 31 +// Output Normalized: No +// Output Max Magnitude: Input magnitude + 1 +func (f *FieldVal) Negate(magnitude uint32) *FieldVal { + f.impl.Negate(magnitude) + return f +} + +// AddInt adds the passed integer to the existing field element and stores the +// result in f in constant time. This is a convenience function since it is +// fairly common to perform some arithmetic with small native integers. +// +// The field element is returned to support chaining. This enables syntax like: +// f.AddInt(1).Add(f2) so that f = f + 1 + f2. +// +// Preconditions: +// - The field element MUST have a max magnitude of 31 +// - The integer MUST be at most 32767 +// Output Normalized: No +// Output Max Magnitude: Existing field magnitude + 1 +func (f *FieldVal) AddInt(ui uint16) *FieldVal { + f.impl.AddInt(ui) + return f +} + +// Add adds the passed element to the existing field element and stores the +// result in f in constant time. +// +// The field element is returned to support chaining. This enables syntax like: +// f.Add(f2).AddInt(1) so that f = f + f2 + 1. +// +// Preconditions: +// - The sum of the magnitudes of the two field elements MUST be at most 32 +// Output Normalized: No +// Output Max Magnitude: Sum of the magnitude of the two individual field elements +func (f *FieldVal) Add(val *FieldVal) *FieldVal { + f.impl.Add(&val.impl) + return f +} + +// Add2 adds the passed two field elements together and stores the result in f +// in constant time. +// +// The field element is returned to support chaining. This enables syntax like: +// f3.Add2(f, f2).AddInt(1) so that f3 = f + f2 + 1. +// +// Preconditions: +// - The sum of the magnitudes of the two field elements MUST be at most 32 +// Output Normalized: No +// Output Max Magnitude: Sum of the magnitude of the two field elements +func (f *FieldVal) Add2(val *FieldVal, val2 *FieldVal) *FieldVal { + f.impl.Add2(&val.impl, &val2.impl) + return f +} + +// MulBy2 multiplies the field element by 2 and stores the result in f in +// constant time. Note that this function can overflow if multiplying the +// element causes any individual limb to overflow uint32. Therefore it is +// important that the caller ensures no overflows will occur before using this +// function. +// +// The field element is returned to support chaining. This enables syntax like: +// f.MulBy2().Add(f2) so that f = 2 * f + f2. +// +// Preconditions: +// - The field element magnitude multiplied by 2 MUST be at most 32 +// Output Normalized: No +// Output Max Magnitude: Existing field magnitude times 2 +func (f *FieldVal) MulBy2() *FieldVal { + f.impl.MulBy2() + return f +} + +// MulBy3 multiplies the field element by 3 and stores the result in f in +// constant time. Note that this function can overflow if multiplying the +// element causes any individual limb to overflow uint32. Therefore it is +// important that the caller ensures no overflows will occur before using this +// function. +// +// The field element is returned to support chaining. This enables syntax like: +// f.MulBy3().Add(f2) so that f = 3 * f + f2. +// +// Preconditions: +// - The field element magnitude multiplied by 3 MUST be at most 32 +// Output Normalized: No +// Output Max Magnitude: Existing field element magnitude times 3 +func (f *FieldVal) MulBy3() *FieldVal { + f.impl.MulBy3() + return f +} + +// MulBy4 multiplies the field element by 4 and stores the result in f in +// constant time. Note that this function can overflow if multiplying the +// element causes any individual limb to overflow uint32. Therefore it is +// important that the caller ensures no overflows will occur before using this +// function. +// +// The field element is returned to support chaining. This enables syntax like: +// f.MulBy4().Add(f2) so that f = 4 * f + f2. +// +// Preconditions: +// - The field element magnitude multiplied by 4 MUST be at most 32 +// Output Normalized: No +// Output Max Magnitude: Existing field element magnitude times 4 +func (f *FieldVal) MulBy4() *FieldVal { + f.impl.MulBy4() + return f +} + +// MulBy8 multiplies the field element by 8 and stores the result in f in +// constant time. Note that this function can overflow if multiplying the +// element causes any individual limb to overflow uint32. Therefore it is +// important that the caller ensures no overflows will occur before using this +// function. +// +// The field element is returned to support chaining. This enables syntax like: +// f.MulBy8().Add(f2) so that f = 8 * f + f2. +// +// Preconditions: +// - The field element magnitude multiplied by 8 MUST be at most 32 +// Output Normalized: No +// Output Max Magnitude: Existing field element magnitude times 8 +func (f *FieldVal) MulBy8() *FieldVal { + f.impl.MulBy8() + return f +} + +// MulInt multiplies the field element by the passed int and stores the result +// in f in constant time. Note that this function can overflow if multiplying +// the element causes any individual limb to overflow uint32. Therefore it is +// important that the caller ensures no overflows will occur before using this +// function. +// +// Callers should prefer using the specialized methods for multiplying by 2, 3, +// 4, and 8, as they are commonly used in curve equations. +// +// See [FieldVal.MulBy2], [FieldVal.MulBy3], [FieldVal.MulBy4], and +// [FieldVal.MulBy8] for the aforementioned specialized methods. +// +// The field element is returned to support chaining. This enables syntax like: +// f.MulInt(2).Add(f2) so that f = 2 * f + f2. +// +// Preconditions: +// - The field element magnitude multiplied by given val MUST be at most 32 +// Output Normalized: No +// Output Max Magnitude: Existing field element magnitude times the provided integer +func (f *FieldVal) MulInt(val uint8) *FieldVal { + f.impl.MulInt(val) + return f +} + +// Mul multiplies the passed element to the existing field element and stores +// the result in f in constant time. Note that this function can overflow if +// multiplying causes any individual limb to overflow uint32. In practice, this +// means the magnitude of either element involved in the multiplication must be +// at most 8. +// +// The field element is returned to support chaining. This enables syntax like: +// f.Mul(f2).AddInt(1) so that f = (f * f2) + 1. +// +// Preconditions: +// - Both field elements MUST have a max magnitude of 8 +// Output Normalized: No +// Output Max Magnitude: 1 +func (f *FieldVal) Mul(val *FieldVal) *FieldVal { + f.impl.Mul(&val.impl) + return f +} + +// Mul2 multiplies the passed two field elements together and stores the result +// in f in constant time. Note that this function can overflow if multiplying +// any of the individual limbs exceeds a max uint32. In practice, this means +// the magnitude of either element involved in the multiplication must be a max +// of 8. +// +// The field element is returned to support chaining. This enables syntax like: +// f3.Mul2(f, f2).AddInt(1) so that f3 = (f * f2) + 1. +// +// Preconditions: +// - Both input field elements MUST have a max magnitude of 8 +// Output Normalized: No +// Output Max Magnitude: 1 +func (f *FieldVal) Mul2(val *FieldVal, val2 *FieldVal) *FieldVal { + f.impl.Mul2(&val.impl, &val2.impl) + return f +} + +// SquareRootVal either calculates the square root of the passed element when it +// exists or the square root of the negation of the element when it does not +// exist and stores the result in f in constant time. The return flag is true +// when the calculated square root is for the passed element itself and false +// when it is for its negation. +// +// Note that this function can overflow if multiplying any of the individual +// limbs exceeds a max uint32. In practice, this means the magnitude of the +// field must be at most 8 to prevent overflow. The magnitude of the result +// will be 1. +// +// Preconditions: +// - The input field element MUST have a max magnitude of 8 +// Output Normalized: No +// Output Max Magnitude: 1 +func (f *FieldVal) SquareRootVal(val *FieldVal) bool { + return f.impl.SquareRootVal(&val.impl) +} + +// Square squares the field element in constant time. The existing field +// element is modified. Note that this function can overflow if multiplying any +// of the individual limbs exceeds a max uint32. In practice, this means the +// magnitude of the field element must be at most 8 to prevent overflow. +// +// The field element is returned to support chaining. This enables syntax like: +// f.Square().Mul(f2) so that f = f^2 * f2. +// +// Preconditions: +// - The field element MUST have a max magnitude of 8 +// Output Normalized: No +// Output Max Magnitude: 1 +func (f *FieldVal) Square() *FieldVal { + f.impl.Square() + return f +} + +// SquareVal squares the passed element and stores the result in f in constant +// time. Note that this function can overflow if multiplying any of the +// individual limbs exceeds a max uint32. In practice, this means the magnitude +// of the field element being squared must be at most 8 to prevent overflow. +// +// The field element is returned to support chaining. This enables syntax like: +// f3.SquareVal(f).Mul(f) so that f3 = f^2 * f = f^3. +// +// Preconditions: +// - The input field element MUST have a max magnitude of 8 +// Output Normalized: No +// Output Max Magnitude: 1 +func (f *FieldVal) SquareVal(val *FieldVal) *FieldVal { + f.impl.SquareVal(&val.impl) + return f +} + +// Inverse finds the modular multiplicative inverse of the field element in +// constant time. The existing field element is modified. +// +// The field element is returned to support chaining. This enables syntax like: +// f.Inverse().Mul(f2) so that f = f^-1 * f2. +// +// Preconditions: +// - The field element MUST have a max magnitude of 8 +// Output Normalized: No +// Output Max Magnitude: 1 +func (f *FieldVal) Inverse() *FieldVal { + f.impl.Inverse() + return f +} + +// IsGtOrEqPrimeMinusOrder returns whether or not the field element is greater +// than or equal to the field prime minus the secp256k1 group order in constant +// time. +// +// Preconditions: +// - The field element MUST be normalized +func (f *FieldVal) IsGtOrEqPrimeMinusOrder() bool { + return f.impl.IsGtOrEqPrimeMinusOrder() +} From cac278c42fae9d5bd1ec6b2622a7579b72ca4ee5 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 29 Jul 2026 01:09:48 -0500 Subject: [PATCH 29/33] secp256k1: Add fieldval wrapper tests. This adds dedicated tests for the new FieldVal wrapper. The tests intentionally do not perform exhaustive testing of each of the methods since that is handled by each implementation. Instead, the focus is on making sure the wrapped methods return the same result as the underlying implementation and the methods that involve chaining return the same wrapped instance. This is part of a series of commits that will ultimately allow architecture-specific backend selection. --- dcrec/secp256k1/fieldwrap_test.go | 302 ++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 dcrec/secp256k1/fieldwrap_test.go diff --git a/dcrec/secp256k1/fieldwrap_test.go b/dcrec/secp256k1/fieldwrap_test.go new file mode 100644 index 0000000000..a72f53edd5 --- /dev/null +++ b/dcrec/secp256k1/fieldwrap_test.go @@ -0,0 +1,302 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package secp256k1 + +import ( + mrand "math/rand" + "testing" + "time" +) + +// assertSameReceiver ensures the wrapper preserves the chaining API. +func assertSameReceiver(t *testing.T, got, want *FieldVal) { + t.Helper() + + if got != want { + t.Fatalf("method returned %p, want %p", got, want) + } +} + +// assertFieldValMatchesImpl ensures the wrapper and underlying implementation +// produce identical normalized values. +func assertFieldValMatchesImpl(t *testing.T, got *FieldVal, want *fieldImpl) { + t.Helper() + + got.Normalize() + want.Normalize() + + if !got.impl.Equals(want) { + t.Fatalf("mismatched values:\n got: %v\nwant: %v", got.impl.String(), + want.String()) + } +} + +// randBytes32 returns a 32-byte array created from a random value generated by +// the passed rng. +func randBytes32(t *testing.T, rng *mrand.Rand) [32]byte { + t.Helper() + + var buf [32]byte + if _, err := rng.Read(buf[:]); err != nil { + t.Fatalf("failed to read random: %v", err) + } + + return buf +} + +// randFieldValAndImplElement returns a [FieldVal] and [fieldImpl] element +// created from a random value generated by the passed rng. +func randFieldValAndImplElement(t *testing.T, rng *mrand.Rand) (*FieldVal, *fieldImpl) { + t.Helper() + + buf := randBytes32(t, rng) + + // Create and return a field value. + var fv FieldVal + fv.SetBytes(&buf) + fv.Normalize() + + var elem fieldImpl + elem.SetBytes(&buf) + elem.Normalize() + + return &fv, &elem +} + +// TestFieldValWrapper ensures the wrapper around the architecture-specific +// field implementation works as intended. It intentionally does NOT perform +// exhaustive testing of each of the methods since that is handled by each +// implementation. Instead, it focuses on making sure the wrapper returns the +// same result as the underlying implementation and the methods that involve +// chaining return the same wrapped instance. +func TestFieldValWrapper(t *testing.T) { + // Use a unique random seed for each test instance and log it if the tests + // fail. + seed := time.Now().Unix() + rng := mrand.New(mrand.NewSource(seed)) + defer func(t *testing.T, seed int64) { + if t.Failed() { + t.Logf("random seed: %d", seed) + } + }(t, seed) + + assertEqBool := func(got, want bool) { + t.Helper() + + if got != want { + t.Fatalf("mismatched result -- got %v, want %v", got, want) + } + } + + assertEqUint32 := func(got, want uint32) { + t.Helper() + + if got != want { + t.Fatalf("mismatched result -- got %v, want %v", got, want) + } + } + + for i := 0; i < 25; i++ { + // Create a field val and underlying implementation element from the + // same random value and immediately ensure they match because the + // helper depends on [FieldVal.SetBytes]. This also indirectly tests + // [FieldVal.Normalize]. + fv, impl := randFieldValAndImplElement(t, rng) + assertFieldValMatchesImpl(t, fv, impl) + + // Ensure [FieldVal.String] forwards properly. This is done early to + // avoid misleading output if anything were to fail later. + if got := fv.String(); got != impl.String() { + t.Errorf("mismatched string -- got %v, want %v", got, impl.String()) + } + + // Ensure [FieldVal.Normalize] forwards and chains properly. + // + // The output of the helper is already normalized, so multiply to + // potentially create internal overflow (for implementations where it + // applies). + // + // Note that this relies on the correctness of [FieldVal.MulInt], but + // that is tested later, so it's safe. + // + // This could be done after [FieldVal.MulInt] is tested to avoid that, + // but it is done early to avoid what could potentially be very + // confusing output otherwise since most of the other tests rely on + // normalization. + fv.MulInt(20) + impl.MulInt(20) + assertSameReceiver(t, fv.Normalize(), fv) + assertFieldValMatchesImpl(t, fv, impl.Normalize()) + + // ---------------------- + // Construction wrappers. + // ---------------------- + + // Ensure [FieldVal.Set] forwards properly. + fvSet, implSet := new(FieldVal).Set(fv), new(fieldImpl).Set(impl) + assertFieldValMatchesImpl(t, fvSet, implSet) + + // Ensure [FieldVal.Zero] forwards properly. + fv.Zero() + impl.Zero() + assertFieldValMatchesImpl(t, fv, impl) + + // Ensure [FieldVal.SetBytes] and [FieldVal.SetByteSlice] forward + // properly with a random value. + b32 := randBytes32(t, rng) + assertEqUint32(fv.SetBytes(&b32), impl.SetBytes(&b32)) + assertFieldValMatchesImpl(t, fv, impl) + assertEqBool(fv.SetByteSlice(b32[:]), impl.SetByteSlice(b32[:])) + assertFieldValMatchesImpl(t, fv, impl) + + // Ensure [FieldVal.SetBytes] and [FieldVal.SetByteSlice] forward + // properly with the field prime to ensure overflow is forwarded + // properly. + curveParams.P.FillBytes(b32[:]) + assertEqUint32(fv.SetBytes(&b32), impl.SetBytes(&b32)) + assertFieldValMatchesImpl(t, fv, impl) + assertEqBool(fv.SetByteSlice(b32[:]), impl.SetByteSlice(b32[:])) + assertFieldValMatchesImpl(t, fv, impl) + + // Ensure [FieldVal.SetBytes] and [FieldVal.SetByteSlice] forward + // properly with the group order to ensure non-overflow is forwarded + // properly. + curveParams.N.FillBytes(b32[:]) + assertEqUint32(fv.SetBytes(&b32), impl.SetBytes(&b32)) + assertFieldValMatchesImpl(t, fv, impl) + assertEqBool(fv.SetByteSlice(b32[:]), impl.SetByteSlice(b32[:])) + assertFieldValMatchesImpl(t, fv, impl) + + // Ensure [FieldVal.SetInt] forwards and chains properly. + assertSameReceiver(t, fv.SetInt(1), fv) + assertFieldValMatchesImpl(t, fv, impl.SetInt(1)) + + // ---------------------- + // Output bytes wrappers. + // ---------------------- + + // Generate new random value since the instances were clobbered above. + fv, impl = randFieldValAndImplElement(t, rng) + + // Ensure [FieldVal.PutBytesUnchecked] forwards properly. + var fvBuf32, implBuf32 [32]byte + fv.PutBytesUnchecked(fvBuf32[:]) + impl.PutBytesUnchecked(implBuf32[:]) + if fvBuf32 != implBuf32 { + t.Fatalf("mismatched result -- got %x, want %x", fvBuf32, implBuf32) + } + + // Ensure [FieldVal.PutBytes] forwards properly. + fvBuf32, implBuf32 = [32]byte{}, [32]byte{} + fv.PutBytes(&fvBuf32) + impl.PutBytes(&implBuf32) + if fvBuf32 != implBuf32 { + t.Fatalf("mismatched result -- got %x, want %x", fvBuf32, implBuf32) + } + + // Ensure [FieldVal.Bytes] forwards properly. + fvBuf32, implBuf32 = *fv.Bytes(), *impl.Bytes() + if fvBuf32 != implBuf32 { + t.Fatalf("mismatched result -- got %x, want %x", fvBuf32, implBuf32) + } + + // -------------------- + // Comparison wrappers. + // -------------------- + + // Ensure [FieldVal.IsZeroBit] forwards properly. + assertEqUint32(fv.IsZeroBit(), impl.IsZeroBit()) + + // Ensure [FieldVal.IsZero] forwards properly. + assertEqBool(fv.IsZero(), impl.IsZero()) + + // Ensure [FieldVal.IsOneBit] forwards properly. + assertEqUint32(fv.IsOneBit(), impl.IsOneBit()) + + // Ensure [FieldVal.IsOne] forwards properly. + assertEqBool(fv.IsOne(), impl.IsOne()) + + // Ensure [FieldVal.IsOddBit] forwards properly. + assertEqUint32(fv.IsOddBit(), impl.IsOddBit()) + + // Ensure [FieldVal.IsOdd] forwards properly. + assertEqBool(fv.IsOdd(), impl.IsOdd()) + + // Ensure [FieldVal.Equals] forwards properly. + fv2, impl2 := randFieldValAndImplElement(t, rng) + assertEqBool(fv.Equals(fv2), impl.Equals(impl2)) + + // Ensure [FieldVal.IsGtOrEqPrimeMinusOrder] forwards properly. + assertEqBool(fv.IsGtOrEqPrimeMinusOrder(), impl.IsGtOrEqPrimeMinusOrder()) + + // -------------------- + // Arithmetic wrappers. + // -------------------- + + // Ensure [FieldVal.NegateVal] forwards and chains properly. + assertSameReceiver(t, fv.NegateVal(fv2, 1), fv) + assertFieldValMatchesImpl(t, fv, impl.NegateVal(impl2, 1)) + + // Ensure [FieldVal.Negate] forwards and chains properly. + assertSameReceiver(t, fv.Negate(1), fv) + assertFieldValMatchesImpl(t, fv, impl.Negate(1)) + + // Ensure [FieldVal.AddInt] forwards and chains properly. + assertSameReceiver(t, fv.AddInt(1), fv) + assertFieldValMatchesImpl(t, fv, impl.AddInt(1)) + + // Ensure [FieldVal.Add] forwards and chains properly. + assertSameReceiver(t, fv.Add(fv2), fv) + assertFieldValMatchesImpl(t, fv, impl.Add(impl2)) + + // Ensure [FieldVal.Add2] forwards and chains properly. + assertSameReceiver(t, fv.Add2(fv, fv2), fv) + assertFieldValMatchesImpl(t, fv, impl.Add2(impl, impl2)) + + // Ensure [FieldVal.MulBy2] forwards and chains properly. + assertSameReceiver(t, fv.MulBy2(), fv) + assertFieldValMatchesImpl(t, fv, impl.MulBy2()) + + // Ensure [FieldVal.MulBy3] forwards and chains properly. + assertSameReceiver(t, fv.MulBy3(), fv) + assertFieldValMatchesImpl(t, fv, impl.MulBy3()) + + // Ensure [FieldVal.MulBy4] forwards and chains properly. + assertSameReceiver(t, fv.MulBy4(), fv) + assertFieldValMatchesImpl(t, fv, impl.MulBy4()) + + // Ensure [FieldVal.MulBy8] forwards and chains properly. + assertSameReceiver(t, fv.MulBy8(), fv) + assertFieldValMatchesImpl(t, fv, impl.MulBy8()) + + // Ensure [FieldVal.MulInt] forwards and chains properly. + assertSameReceiver(t, fv.MulInt(16), fv) + assertFieldValMatchesImpl(t, fv, impl.MulInt(16)) + + // Ensure [FieldVal.Mul] forwards and chains properly. + assertSameReceiver(t, fv.Mul(fv2), fv) + assertFieldValMatchesImpl(t, fv, impl.Mul(impl2)) + + // Ensure [FieldVal.Mul2] forwards and chains properly. + assertSameReceiver(t, fv.Mul2(fv, fv2), fv) + assertFieldValMatchesImpl(t, fv, impl.Mul2(impl, impl2)) + + // Ensure [FieldVal.SquareRootVal] forwards properly. + assertEqBool(fv.SquareRootVal(fv2), impl.SquareRootVal(impl2)) + assertFieldValMatchesImpl(t, fv, impl) + + // Ensure [FieldVal.Square] forwards and chains properly. + assertSameReceiver(t, fv.Square(), fv) + assertFieldValMatchesImpl(t, fv, impl.Square()) + + // Ensure [FieldVal.SquareVal] forwards and chains properly. + assertSameReceiver(t, fv.SquareVal(fv2), fv) + assertFieldValMatchesImpl(t, fv, impl.SquareVal(impl2)) + + // Ensure [FieldVal.Inverse] forwards and chains properly. + assertSameReceiver(t, fv.Inverse(), fv) + assertFieldValMatchesImpl(t, fv, impl.Inverse()) + } +} From 6d1c792ab7191456d5cf94ca7f97582b248c4b67 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 29 Jul 2026 01:09:49 -0500 Subject: [PATCH 30/33] secp256k1: Add field10x26 README.md. This is part of a series of commits that will ultimately allow architecture-specific backend selection. --- dcrec/secp256k1/field10x26/README.md | 62 ++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 dcrec/secp256k1/field10x26/README.md diff --git a/dcrec/secp256k1/field10x26/README.md b/dcrec/secp256k1/field10x26/README.md new file mode 100644 index 0000000000..a5e499e8b9 --- /dev/null +++ b/dcrec/secp256k1/field10x26/README.md @@ -0,0 +1,62 @@ +field10x26 +========== + +[![Build Status](https://github.com/decred/dcrd/workflows/Build%20and%20Test/badge.svg)](https://github.com/decred/dcrd/actions) +[![ISC License](https://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![Doc](https://img.shields.io/badge/doc-reference-blue.svg)](https://pkg.go.dev/github.com/decred/dcrd/dcrec/secp256k1/v4/field10x26) + +Package field10x26 provides highly optimized pure-Go arithmetic over the +secp256k1 finite field using a 10x26 representation. + +It is designed for correctness, performance, security, and high assurance +through specialized arithmetic, constant time engineering, differential testing, +and multiple complementary testing techniques. + +This package exposes the underlying implementation directly and is primarily +intended for specialized use cases. Most consumers should prefer the +`secp256k1` package, which provides the stable public `FieldVal` type and +automatically selects the appropriate backend implementation for the target +architecture. + +## Design + +The implementation represents field elements using ten base-2^26 limbs with +carefully bounded intermediate overflow between normalizations. This approach +minimizes carry propagation during common arithmetic operations and enables +efficient constant-time implementations. + +Unlike implementations that always maintain canonical field elements, this +representation intentionally permits bounded overflow between normalizations. +Consequently, callers are responsible for observing the documented +normalization, magnitude, and operation preconditions described by the `Element` +type. + +The semantics are a deliberate design choice that eliminates unnecessary work +from performance-critical arithmetic by allowing callers to normalize only when +required. + +## Assurance + +This package emphasizes correctness and implementation assurance through +multiple complementary validation techniques. + +Key implementation characteristics include: + +- Highly optimized arithmetic specialized specifically for the secp256k1 field +- Constant time implementation suitable for secret-dependent operations +- Differential testing against independent secp256k1 implementations +- Deterministic test vectors +- Property-based testing +- Randomized-input testing +- Coverage-guided fuzz testing +- Manual review of security-critical implementation details + +## Installation and Updating + +This package is part of the `github.com/decred/dcrd/dcrec/secp256k1/v4` module. +Use the standard go tooling for working with modules to incorporate it. + +## License + +Package field10x26 is licensed under the [copyfree](http://copyfree.org) ISC +License. From 675f3336eb4e6581ec2f0a885c251f646037cce4 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 29 Jul 2026 01:09:50 -0500 Subject: [PATCH 31/33] secp256k1: Move 4x64 field code to subpackage. This moves the entire FieldVal64 implementation, tests, and benchmarks into a separate package named field4x64 and only makes the minimal required changes to ensure the code compiles and all tests pass. It also modifies the Normalize method to match the signature used by FieldVal for API parity. No functional changes are intended by this commit. It is strictly a refactoring that preserves the existing behavior. This is part of a series of commits that will ultimately allow architecture-specific backend selection. --- dcrec/secp256k1/field4x64/common_test.go | 52 +++++++++++++++++++ dcrec/secp256k1/{ => field4x64}/field64.go | 16 ++++-- .../{ => field4x64}/field64_amd64.go | 2 +- .../secp256k1/{ => field4x64}/field64_amd64.s | 0 .../{ => field4x64}/field64_amd64_test.go | 2 +- .../field64_bench_amd64_test.go | 2 +- .../{ => field4x64}/field64_bench_test.go | 2 +- .../{ => field4x64}/field64_generic.go | 2 +- .../{ => field4x64}/field64_noasm.go | 2 +- .../secp256k1/{ => field4x64}/field64_test.go | 2 +- .../{ => field4x64}/field64isa_amd64.go | 2 +- 11 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 dcrec/secp256k1/field4x64/common_test.go rename dcrec/secp256k1/{ => field4x64}/field64.go (98%) rename dcrec/secp256k1/{ => field4x64}/field64_amd64.go (96%) rename dcrec/secp256k1/{ => field4x64}/field64_amd64.s (100%) rename dcrec/secp256k1/{ => field4x64}/field64_amd64_test.go (99%) rename dcrec/secp256k1/{ => field4x64}/field64_bench_amd64_test.go (99%) rename dcrec/secp256k1/{ => field4x64}/field64_bench_test.go (99%) rename dcrec/secp256k1/{ => field4x64}/field64_generic.go (99%) rename dcrec/secp256k1/{ => field4x64}/field64_noasm.go (97%) rename dcrec/secp256k1/{ => field4x64}/field64_test.go (99%) rename dcrec/secp256k1/{ => field4x64}/field64isa_amd64.go (97%) diff --git a/dcrec/secp256k1/field4x64/common_test.go b/dcrec/secp256k1/field4x64/common_test.go new file mode 100644 index 0000000000..f279d619d8 --- /dev/null +++ b/dcrec/secp256k1/field4x64/common_test.go @@ -0,0 +1,52 @@ +// Copyright (c) 2026 The Decred developers +// Copyright (c) 2013-2026 Dave Collins +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package field4x64 + +// References: +// [SECG]: Recommended Elliptic Curve Domain Parameters +// https://www.secg.org/sec2-v2.pdf +// + +import ( + "encoding/hex" + "math/big" +) + +// Curve parameters taken from [SECG] section 2.4.1. +var curveParams = struct { + P *big.Int + N *big.Int +}{ + P: fromHex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"), + N: fromHex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"), +} + +// hexToBytes converts the passed hex string into bytes and will panic if there +// is an error. This is only provided for the hard-coded constants so errors in +// the source code can be detected. It will only (and must only) be called with +// hard-coded values. +func hexToBytes(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic("invalid hex in source file: " + s) + } + return b +} + +// fromHex converts the passed hex string into a big integer pointer and will +// panic is there is an error. This is only provided for the hard-coded +// constants so errors in the source code can bet detected. It will only (and +// must only) be called for initialization purposes. +func fromHex(s string) *big.Int { + if s == "" { + return big.NewInt(0) + } + r, ok := new(big.Int).SetString(s, 16) + if !ok { + panic("invalid hex in source file: " + s) + } + return r +} diff --git a/dcrec/secp256k1/field64.go b/dcrec/secp256k1/field4x64/field64.go similarity index 98% rename from dcrec/secp256k1/field64.go rename to dcrec/secp256k1/field4x64/field64.go index e6bdef1ffd..8672882156 100644 --- a/dcrec/secp256k1/field64.go +++ b/dcrec/secp256k1/field4x64/field64.go @@ -2,7 +2,9 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package secp256k1 +// Package field4x64 implements highly optimized, constant-time arithmetic over +// the secp256k1 finite field using a dense 4x64 representation. +package field4x64 import ( "encoding/binary" @@ -146,6 +148,11 @@ func (f *FieldVal64) SetBytes(b *[32]byte) uint32 { return uint32(1 - borrow) } +// zeroArray32 zeroes the provided 32-byte buffer. +func zeroArray32(b *[32]byte) { + *b = [32]byte{} +} + // SetByteSlice interprets the provided slice as a 256-bit big-endian unsigned // integer (meaning it is truncated to the first 32 bytes), packs it into the // internal field value representation, and returns whether or not the resulting @@ -168,8 +175,11 @@ func (f *FieldVal64) SetByteSlice(b []byte) bool { return result != 0 } -// Normalize is a no-op. It is provided to keep API parity with [FieldVal]. -func (f *FieldVal64) Normalize() {} +// Normalize is a no-op. It is provided to keep API parity for +// [secp256k1.FieldVal]. +func (f *FieldVal64) Normalize() *FieldVal64 { + return f +} // PutBytesUnchecked unpacks the field value to a 32-byte big-endian value // directly into the passed byte slice in constant time. The target slice must diff --git a/dcrec/secp256k1/field64_amd64.go b/dcrec/secp256k1/field4x64/field64_amd64.go similarity index 96% rename from dcrec/secp256k1/field64_amd64.go rename to dcrec/secp256k1/field4x64/field64_amd64.go index 1774419d9b..c939d52723 100644 --- a/dcrec/secp256k1/field64_amd64.go +++ b/dcrec/secp256k1/field4x64/field64_amd64.go @@ -4,7 +4,7 @@ //go:build !purego -package secp256k1 +package field4x64 import ( "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/cpufeat" diff --git a/dcrec/secp256k1/field64_amd64.s b/dcrec/secp256k1/field4x64/field64_amd64.s similarity index 100% rename from dcrec/secp256k1/field64_amd64.s rename to dcrec/secp256k1/field4x64/field64_amd64.s diff --git a/dcrec/secp256k1/field64_amd64_test.go b/dcrec/secp256k1/field4x64/field64_amd64_test.go similarity index 99% rename from dcrec/secp256k1/field64_amd64_test.go rename to dcrec/secp256k1/field4x64/field64_amd64_test.go index 90f3c4f2fc..2e40804624 100644 --- a/dcrec/secp256k1/field64_amd64_test.go +++ b/dcrec/secp256k1/field4x64/field64_amd64_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package secp256k1 +package field4x64 import "testing" diff --git a/dcrec/secp256k1/field64_bench_amd64_test.go b/dcrec/secp256k1/field4x64/field64_bench_amd64_test.go similarity index 99% rename from dcrec/secp256k1/field64_bench_amd64_test.go rename to dcrec/secp256k1/field4x64/field64_bench_amd64_test.go index 5899576b48..e02659fb20 100644 --- a/dcrec/secp256k1/field64_bench_amd64_test.go +++ b/dcrec/secp256k1/field4x64/field64_bench_amd64_test.go @@ -4,7 +4,7 @@ //go:build amd64 && !purego -package secp256k1 +package field4x64 import ( "testing" diff --git a/dcrec/secp256k1/field64_bench_test.go b/dcrec/secp256k1/field4x64/field64_bench_test.go similarity index 99% rename from dcrec/secp256k1/field64_bench_test.go rename to dcrec/secp256k1/field4x64/field64_bench_test.go index cf24ca9846..e285b938a4 100644 --- a/dcrec/secp256k1/field64_bench_test.go +++ b/dcrec/secp256k1/field4x64/field64_bench_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package secp256k1 +package field4x64 import ( "testing" diff --git a/dcrec/secp256k1/field64_generic.go b/dcrec/secp256k1/field4x64/field64_generic.go similarity index 99% rename from dcrec/secp256k1/field64_generic.go rename to dcrec/secp256k1/field4x64/field64_generic.go index 3aa751235e..faa45d28cd 100644 --- a/dcrec/secp256k1/field64_generic.go +++ b/dcrec/secp256k1/field4x64/field64_generic.go @@ -2,7 +2,7 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package secp256k1 +package field4x64 import ( "math/bits" diff --git a/dcrec/secp256k1/field64_noasm.go b/dcrec/secp256k1/field4x64/field64_noasm.go similarity index 97% rename from dcrec/secp256k1/field64_noasm.go rename to dcrec/secp256k1/field4x64/field64_noasm.go index 840a84826a..e2cb511ef7 100644 --- a/dcrec/secp256k1/field64_noasm.go +++ b/dcrec/secp256k1/field4x64/field64_noasm.go @@ -4,7 +4,7 @@ //go:build !amd64 || purego -package secp256k1 +package field4x64 // field64UseBMI2AndADX depends on hardware support and access to assembly // instructions. diff --git a/dcrec/secp256k1/field64_test.go b/dcrec/secp256k1/field4x64/field64_test.go similarity index 99% rename from dcrec/secp256k1/field64_test.go rename to dcrec/secp256k1/field4x64/field64_test.go index c2ff02d0f3..052a29aeb2 100644 --- a/dcrec/secp256k1/field64_test.go +++ b/dcrec/secp256k1/field4x64/field64_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package secp256k1 +package field4x64 import ( "bytes" diff --git a/dcrec/secp256k1/field64isa_amd64.go b/dcrec/secp256k1/field4x64/field64isa_amd64.go similarity index 97% rename from dcrec/secp256k1/field64isa_amd64.go rename to dcrec/secp256k1/field4x64/field64isa_amd64.go index 8f93dbefed..0d3bc6c2f9 100644 --- a/dcrec/secp256k1/field64isa_amd64.go +++ b/dcrec/secp256k1/field4x64/field64isa_amd64.go @@ -4,7 +4,7 @@ //go:build !purego -package secp256k1 +package field4x64 // field64MulReduce sets r = a * b (mod p) using processor-specific hardware // extensions when available. From 94db16e7e985b197b50102c1a7d9ba30d7836ab7 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 29 Jul 2026 01:09:50 -0500 Subject: [PATCH 32/33] secp256k1: Rename field4x64.FieldVal64 to Element. This renames FieldVal64 in the new field4x64 package to Element to more accurately reflect that it is an implementation-specific field element rather than the public compatibility type. It also renames all tests and benchmarks to match and updates various comments accordingly. It is a large change in terms of lines changed, but it is entirely mechanical. No functional changes are intended by this commit. It is strictly a refactoring that preserves the existing behavior. This is part of a series of commits that will ultimately allow architecture-specific backend selection. --- dcrec/secp256k1/field10x26/element.go | 2 +- .../field4x64/{field64.go => element.go} | 619 +++++++++--------- .../{field64_amd64.go => element_amd64.go} | 10 +- .../{field64_amd64.s => element_amd64.s} | 8 +- ...64_amd64_test.go => element_amd64_test.go} | 12 +- ...64_test.go => element_bench_amd64_test.go} | 31 +- .../secp256k1/field4x64/element_bench_test.go | 175 +++++ ...{field64_generic.go => element_generic.go} | 44 +- dcrec/secp256k1/field4x64/element_noasm.go | 23 + .../{field64_test.go => element_test.go} | 474 ++++++++------ dcrec/secp256k1/field4x64/elementisa_amd64.go | 27 + .../secp256k1/field4x64/field64_bench_test.go | 175 ----- dcrec/secp256k1/field4x64/field64_noasm.go | 23 - dcrec/secp256k1/field4x64/field64isa_amd64.go | 27 - .../proofs/field_4x64_reduce_prover.py | 56 +- 15 files changed, 898 insertions(+), 808 deletions(-) rename dcrec/secp256k1/field4x64/{field64.go => element.go} (57%) rename dcrec/secp256k1/field4x64/{field64_amd64.go => element_amd64.go} (57%) rename dcrec/secp256k1/field4x64/{field64_amd64.s => element_amd64.s} (94%) rename dcrec/secp256k1/field4x64/{field64_amd64_test.go => element_amd64_test.go} (84%) rename dcrec/secp256k1/field4x64/{field64_bench_amd64_test.go => element_bench_amd64_test.go} (56%) create mode 100644 dcrec/secp256k1/field4x64/element_bench_test.go rename dcrec/secp256k1/field4x64/{field64_generic.go => element_generic.go} (75%) create mode 100644 dcrec/secp256k1/field4x64/element_noasm.go rename dcrec/secp256k1/field4x64/{field64_test.go => element_test.go} (75%) create mode 100644 dcrec/secp256k1/field4x64/elementisa_amd64.go delete mode 100644 dcrec/secp256k1/field4x64/field64_bench_test.go delete mode 100644 dcrec/secp256k1/field4x64/field64_noasm.go delete mode 100644 dcrec/secp256k1/field4x64/field64isa_amd64.go diff --git a/dcrec/secp256k1/field10x26/element.go b/dcrec/secp256k1/field10x26/element.go index 1d40e5da4a..0682b8604e 100644 --- a/dcrec/secp256k1/field10x26/element.go +++ b/dcrec/secp256k1/field10x26/element.go @@ -42,7 +42,7 @@ import ( // While both of those things are still true without intrinsics, modern Go now // provides access to intrinsics that permit the hardware to perform both full // 128-bit products and addition with carry which entirely mitigates those -// limitations. As a result, there is now an alternative [secp256k1.FieldVal64] +// limitations. As a result, there is now an alternative [field4x64.Element] // implementation that uses the aforementioned 4x64 representation with // intrinsics. // diff --git a/dcrec/secp256k1/field4x64/field64.go b/dcrec/secp256k1/field4x64/element.go similarity index 57% rename from dcrec/secp256k1/field4x64/field64.go rename to dcrec/secp256k1/field4x64/element.go index 8672882156..c42071e22b 100644 --- a/dcrec/secp256k1/field4x64/field64.go +++ b/dcrec/secp256k1/field4x64/element.go @@ -22,15 +22,15 @@ import ( // It uses tight 256-bit packing with four little-endian uint64s and fully // reduces after each operation. Hardware intrinsics are used when available. -// FieldVal64 implements optimized fixed-precision arithmetic over the -// secp256k1 finite field. This means all arithmetic is performed modulo +// Element implements optimized fixed-precision arithmetic over the secp256k1 +// finite field. This means all arithmetic is performed modulo // // 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f. // -// Unlike [FieldVal], this fully reduces after each operation and therefore does -// not require normalization or manual magnitude tracking. It is also quite a -// bit faster than [FieldVal] on all modern 64-bit hardware. -type FieldVal64 struct { +// This fully reduces after each operation and therefore does not require +// normalization or manual magnitude tracking. It is also quite a bit faster +// than [field10x26.Element] on all modern 64-bit hardware. +type Element struct { // Each 256-bit value is represented as 4 64-bit integers in base 2^64. // It only implements the arithmetic needed for elliptic curve operations. // @@ -56,95 +56,95 @@ type FieldVal64 struct { n [4]uint64 } -// Constants related to the field representation. +// Constants related to the internal representation. const ( - // field64PrimeComplement is the two's complement of the secp256k1 prime. - field64PrimeComplement = 0x1000003d1 // 2^32 + 977 + // fieldPrimeComplement is the two's complement of the secp256k1 prime. + fieldPrimeComplement = 0x1000003d1 // 2^32 + 977 // These fields provide convenient access to each of the limbs of the - // secp256k1 prime in the internal field representation to improve code + // secp256k1 prime in the internal representation to improve code // readability. - field64Prime0 = 0xfffffffefffffc2f - field64Prime1 = 0xffffffffffffffff - field64Prime2 = 0xffffffffffffffff - field64Prime3 = 0xffffffffffffffff + fieldPrimeLimb0 = 0xfffffffefffffc2f + fieldPrimeLimb1 = 0xffffffffffffffff + fieldPrimeLimb2 = 0xffffffffffffffff + fieldPrimeLimb3 = 0xffffffffffffffff ) -// String returns the field value as a human-readable hex string. -func (f FieldVal64) String() string { - return hex.EncodeToString(f.Bytes()[:]) +// String returns the element as a human-readable hex string. +func (e Element) String() string { + return hex.EncodeToString(e.Bytes()[:]) } -// Zero sets the field value to zero in constant time. A newly created field -// value is already set to zero. This function can be useful to clear an -// existing field value for reuse. -func (f *FieldVal64) Zero() { - f.n = [4]uint64{} +// Zero sets the element to zero in constant time. A newly created element is +// already set to zero. This function can be useful to clear an existing +// element for reuse. +func (e *Element) Zero() { + e.n = [4]uint64{} } -// Set sets the field value equal to the passed value in constant time. +// Set sets the element equal to the passed element in constant time. // -// The field value is returned to support chaining. This enables syntax like: -// f := new(FieldVal).Set(f2).Add(1) so that f = f2 + 1 where f2 is not +// The element is returned to support chaining. This enables syntax like: +// e := new(Element).Set(e2).Add(1) so that e = e2 + 1 where e2 is not // modified. -func (f *FieldVal64) Set(val *FieldVal64) *FieldVal64 { - f.n = val.n - return f +func (e *Element) Set(val *Element) *Element { + e.n = val.n + return e } -// SetInt sets the field value to the passed integer in constant time. This is -// a convenience function since it is fairly common to perform some arithmetic -// with small native integers. +// SetInt sets the element to the passed integer in constant time. This is a +// convenience function since it is fairly common to perform arithmetic with +// small native integers. // -// The field value is returned to support chaining. This enables syntax such -// as f := new(FieldVal).SetInt(2).Mul(f2) so that f = 2 * f2. -func (f *FieldVal64) SetInt(v uint16) *FieldVal64 { - f.n = [4]uint64{uint64(v), 0, 0, 0} - return f +// The element is returned to support chaining. This enables syntax such +// as e := new(Element).SetInt(2).Mul(e2) so that e = 2 * e2. +func (e *Element) SetInt(v uint16) *Element { + e.n = [4]uint64{uint64(v), 0, 0, 0} + return e } -// SetBytes packs the passed 32-byte big-endian value into the internal field -// value representation in constant time. It interprets the provided array as a -// 256-bit big-endian unsigned integer, packs it into the internal field value -// representation, and returns either 1 if it is greater than or equal to the -// field prime (aka it overflowed) or 0 otherwise in constant time. +// SetBytes packs the passed 32-byte big-endian value into the internal +// representation in constant time. It interprets the provided array as a +// 256-bit big-endian unsigned integer, packs it, and returns either 1 if it is +// greater than or equal to the field prime (aka it overflowed) or 0 otherwise +// in constant time. // // Note that a bool is not used here because it is not possible in Go to convert // from a bool to numeric value in constant time and many constant-time // operations require a numeric value. -func (f *FieldVal64) SetBytes(b *[32]byte) uint32 { +func (e *Element) SetBytes(b *[32]byte) uint32 { // Pack the 256 total bits across the 4 uint64 limbs. - f.n[0] = binary.BigEndian.Uint64(b[24:32]) - f.n[1] = binary.BigEndian.Uint64(b[16:24]) - f.n[2] = binary.BigEndian.Uint64(b[8:16]) - f.n[3] = binary.BigEndian.Uint64(b[0:8]) + e.n[0] = binary.BigEndian.Uint64(b[24:32]) + e.n[1] = binary.BigEndian.Uint64(b[16:24]) + e.n[2] = binary.BigEndian.Uint64(b[8:16]) + e.n[3] = binary.BigEndian.Uint64(b[0:8]) - // Since f < 2^256 < 2p (where p is the secp256k1 prime), the max possible + // Since e < 2^256 < 2p (where p is the secp256k1 prime), the max possible // number of reductions required is one. Therefore, in the case a reduction // is needed, it can be performed with a single subtraction of p. // - // Since p must only conditionally be subtracted when f ≥ p, the following - // handles it in constant time by always calculating s = f - p and selecting + // Since p must only conditionally be subtracted when e ≥ p, the following + // handles it in constant time by always calculating s = e - p and selecting // the correct case via a constant time select. - // Subtract p with borrow propagation. borrow is set iff f < p. + // Subtract p with borrow propagation. borrow is set iff e < p. // - // In other words, the input overflowed (≥ p) when f - p does NOT borrow. + // In other words, the input overflowed (≥ p) when e - p does NOT borrow. // - // s = f - p + // s = e - p var s0, s1, s2, s3, borrow uint64 - s0, borrow = bits.Sub64(f.n[0], field64Prime0, 0) - s1, borrow = bits.Sub64(f.n[1], field64Prime1, borrow) - s2, borrow = bits.Sub64(f.n[2], field64Prime2, borrow) - s3, borrow = bits.Sub64(f.n[3], field64Prime3, borrow) + s0, borrow = bits.Sub64(e.n[0], fieldPrimeLimb0, 0) + s1, borrow = bits.Sub64(e.n[1], fieldPrimeLimb1, borrow) + s2, borrow = bits.Sub64(e.n[2], fieldPrimeLimb2, borrow) + s3, borrow = bits.Sub64(e.n[3], fieldPrimeLimb3, borrow) // Constant-time select. // - // Set f = f when f < p (aka borrow is set). Otherwise f = s = f - p. - f.n[0] = arith.ConstantTimeSelect64(borrow, f.n[0], s0) - f.n[1] = arith.ConstantTimeSelect64(borrow, f.n[1], s1) - f.n[2] = arith.ConstantTimeSelect64(borrow, f.n[2], s2) - f.n[3] = arith.ConstantTimeSelect64(borrow, f.n[3], s3) + // Set e = e when e < p (aka borrow is set). Otherwise e = s = e - p. + e.n[0] = arith.ConstantTimeSelect64(borrow, e.n[0], s0) + e.n[1] = arith.ConstantTimeSelect64(borrow, e.n[1], s1) + e.n[2] = arith.ConstantTimeSelect64(borrow, e.n[2], s2) + e.n[3] = arith.ConstantTimeSelect64(borrow, e.n[3], s3) return uint32(1 - borrow) } @@ -155,8 +155,8 @@ func zeroArray32(b *[32]byte) { // SetByteSlice interprets the provided slice as a 256-bit big-endian unsigned // integer (meaning it is truncated to the first 32 bytes), packs it into the -// internal field value representation, and returns whether or not the resulting -// truncated 256-bit integer is greater than or equal to the field prime (aka it +// internal representation, and returns whether or not the resulting truncated +// 256-bit integer is greater than or equal to the field prime (aka it // overflowed) in constant time. // // Note that since passing a slice with more than 32 bytes is truncated, it is @@ -165,148 +165,145 @@ func zeroArray32(b *[32]byte) { // caller to decide whether it needs to provide numbers of the appropriate size // or it if is acceptable to use this function with the described truncation and // overflow behavior. -func (f *FieldVal64) SetByteSlice(b []byte) bool { +func (e *Element) SetByteSlice(b []byte) bool { var b32 [32]byte b = b[:arith.ConstantTimeMin(uint32(len(b)), 32)] copy(b32[:], b32[:32-len(b)]) copy(b32[32-len(b):], b) - result := f.SetBytes(&b32) + result := e.SetBytes(&b32) zeroArray32(&b32) return result != 0 } -// Normalize is a no-op. It is provided to keep API parity for -// [secp256k1.FieldVal]. -func (f *FieldVal64) Normalize() *FieldVal64 { - return f +// Normalize is a no-op. It is provided to keep API parity with the other field +// element implementations. +func (e *Element) Normalize() *Element { + return e } -// PutBytesUnchecked unpacks the field value to a 32-byte big-endian value -// directly into the passed byte slice in constant time. The target slice must -// have at least 32 bytes available or it will panic. +// PutBytesUnchecked unpacks the element to a 32-byte big-endian value directly +// into the passed byte slice in constant time. The target slice must have at +// least 32 bytes available or it will panic. // -// There is a similar function, [FieldVal64.PutBytes], which unpacks the field -// value into a 32-byte array directly. This version is provided since it can -// be useful to write directly into part of a larger buffer without needing a +// There is a similar function, [Element.PutBytes], which unpacks the element +// into a 32-byte array directly. This version is provided since it can be +// useful to write directly into part of a larger buffer without needing a // separate allocation. -func (f *FieldVal64) PutBytesUnchecked(b []byte) { +func (e *Element) PutBytesUnchecked(b []byte) { // Unpack the 256 total bits from the 4 uint64 limbs. - binary.BigEndian.PutUint64(b[0:8], f.n[3]) - binary.BigEndian.PutUint64(b[8:16], f.n[2]) - binary.BigEndian.PutUint64(b[16:24], f.n[1]) - binary.BigEndian.PutUint64(b[24:32], f.n[0]) + binary.BigEndian.PutUint64(b[0:8], e.n[3]) + binary.BigEndian.PutUint64(b[8:16], e.n[2]) + binary.BigEndian.PutUint64(b[16:24], e.n[1]) + binary.BigEndian.PutUint64(b[24:32], e.n[0]) } -// PutBytes unpacks the field value to a 32-byte big-endian value using the -// passed byte array in constant time. +// PutBytes unpacks the element to a 32-byte big-endian value using the passed +// byte array in constant time. // -// There is a similar function, [FieldVal64.PutBytesUnchecked], which unpacks -// the field value into a slice that must have at least 32 bytes available. -// This version is provided since it can be useful to write directly into an -// array that is type checked. +// There is a similar function, [Element.PutBytesUnchecked], which unpacks the +// element into a slice that must have at least 32 bytes available. This +// version is provided since it can be useful to write directly into an array +// that is type checked. // -// Alternatively, there is also [FieldVal64.Bytes], which unpacks the field -// value into a new array and returns that which can sometimes be more ergonomic -// in applications that aren't concerned about an additional copy. -func (f *FieldVal64) PutBytes(b *[32]byte) { - f.PutBytesUnchecked(b[:]) +// Alternatively, there is also [Element.Bytes], which unpacks the element into +// a new array and returns that which can sometimes be more ergonomic in +// applications that aren't concerned about an additional copy. +func (e *Element) PutBytes(b *[32]byte) { + e.PutBytesUnchecked(b[:]) } -// Bytes unpacks the field value to a 32-byte big-endian value in constant time. +// Bytes unpacks the element to a 32-byte big-endian value in constant time. // -// See [FieldVal64.PutBytes] and [FieldVal64.PutBytesUnchecked] for variants -// that allow an array or slice to be passed which can be useful to cut down on -// the number of allocations by allowing the caller to reuse a buffer or write +// See [Element.PutBytes] and [Element.PutBytesUnchecked] for variants that +// allow an array or slice to be passed which can be useful to cut down on the +// number of allocations by allowing the caller to reuse a buffer or write // directly into part of a larger buffer. -func (f *FieldVal64) Bytes() *[32]byte { +func (e *Element) Bytes() *[32]byte { var b [32]byte - f.PutBytesUnchecked(b[:]) + e.PutBytesUnchecked(b[:]) return &b } -// IsZeroBit returns 1 when the field value is equal to zero or 0 otherwise in +// IsZeroBit returns 1 when the element is equal to zero or 0 otherwise in // constant time. // // Note that a bool is not used here because it is not possible in Go to convert // from a bool to numeric value in constant time and many constant-time -// operations require a numeric value. See IsZero for the version that returns -// a bool. -func (f *FieldVal64) IsZeroBit() uint32 { - return arith.ConstantTimeEq64(f.n[0]|f.n[1]|f.n[2]|f.n[3], 0) +// operations require a numeric value. See [Element.IsZero] for the version +// that returns a bool. +func (e *Element) IsZeroBit() uint32 { + return arith.ConstantTimeEq64(e.n[0]|e.n[1]|e.n[2]|e.n[3], 0) } -// IsZero returns whether or not the field value is equal to zero in constant -// time. -func (f *FieldVal64) IsZero() bool { - return (f.n[0] | f.n[1] | f.n[2] | f.n[3]) == 0 +// IsZero returns whether or not the element is equal to zero in constant time. +func (e *Element) IsZero() bool { + return (e.n[0] | e.n[1] | e.n[2] | e.n[3]) == 0 } -// IsOneBit returns 1 when the field value is equal to one or 0 otherwise in +// IsOneBit returns 1 when the element is equal to one or 0 otherwise in // constant time. // // Note that a bool is not used here because it is not possible in Go to convert // from a bool to numeric value in constant time and many constant-time -// operations require a numeric value. See [FieldVal64.IsOne] for the version +// operations require a numeric value. See [Element.IsOne] for the version // that returns a bool. -func (f *FieldVal64) IsOneBit() uint32 { - // The value can only be one if the single lowest significant bit is set in - // the first word and no other bits are set in any of the other words. +func (e *Element) IsOneBit() uint32 { + // The element can only be one if the single lowest significant bit is set + // in the first limb and no other bits are set in any of the other limbs. // This is a constant time implementation. - return arith.ConstantTimeEq64((f.n[0]^1)|f.n[1]|f.n[2]|f.n[3], 0) + return arith.ConstantTimeEq64((e.n[0]^1)|e.n[1]|e.n[2]|e.n[3], 0) } -// IsOne returns whether or not the field value is equal to one in constant -// time. -func (f *FieldVal64) IsOne() bool { - // The value can only be one if the single lowest significant bit is set in - // the first word and no other bits are set in any of the other words. +// IsOne returns whether or not the element is equal to one in constant time. +func (e *Element) IsOne() bool { + // The element can only be one if the single lowest significant bit is set + // in the first limb and no other bits are set in any of the other limbs. // This is a constant time implementation. - return ((f.n[0] ^ 1) | f.n[1] | f.n[2] | f.n[3]) == 0 + return ((e.n[0] ^ 1) | e.n[1] | e.n[2] | e.n[3]) == 0 } -// IsOddBit returns 1 when the field value is an odd number or 0 otherwise in +// IsOddBit returns 1 when the element is an odd number or 0 otherwise in // constant time. // // Note that a bool is not used here because it is not possible in Go to convert // from a bool to numeric value in constant time and many constant-time -// operations require a numeric value. See [FieldVal64.IsOdd] for the version -// that returns a bool. -func (f *FieldVal64) IsOddBit() uint32 { +// operations require a numeric value. See [Element.IsOdd] for the version that +// returns a bool. +func (e *Element) IsOddBit() uint32 { // Only odd numbers have the bottom bit set. - return uint32(f.n[0] & 1) + return uint32(e.n[0] & 1) } -// IsOdd returns whether or not the field value is an odd number in constant -// time. -func (f *FieldVal64) IsOdd() bool { +// IsOdd returns whether or not the element is an odd number in constant time. +func (e *Element) IsOdd() bool { // Only odd numbers have the bottom bit set. - return f.n[0]&1 == 1 + return e.n[0]&1 == 1 } -// Equals returns whether or not the two field values are the same in constant -// time. -func (f *FieldVal64) Equals(val *FieldVal64) bool { - // Xor only sets bits when they are different, so the two field values - // can only be the same if no bits are set after xoring each word. - // This is a constant time implementation. - return ((f.n[0] ^ val.n[0]) | (f.n[1] ^ val.n[1]) | (f.n[2] ^ val.n[2]) | - (f.n[3] ^ val.n[3])) == 0 +// Equals returns whether or not the two elements are the same in constant time. +func (e *Element) Equals(val *Element) bool { + // Xor only sets bits when they are different, so the two elements can only + // be the same if no bits are set after xoring each limb. This is a + // constant time implementation. + return ((e.n[0] ^ val.n[0]) | (e.n[1] ^ val.n[1]) | (e.n[2] ^ val.n[2]) | + (e.n[3] ^ val.n[3])) == 0 } -// NegateVal negates the passed value and stores the result in f in constant -// time. The ignored parameter exists to keep API parity with [FieldVal]. +// NegateVal negates the passed element and stores the result in e in constant +// time. The ignored parameter exists to keep API parity with the field element +// implementations. // -// The field value is returned to support chaining. This enables syntax like: -// f.NegateVal(f2).AddInt(1) so that f = -f2 + 1. -func (f *FieldVal64) NegateVal(val *FieldVal64, _ uint32) *FieldVal64 { - // Since the value is already in the range 0 ≤ val < p, where p is the +// The element is returned to support chaining. This enables syntax like: +// e.NegateVal(e2).AddInt(1) so that e = -e2 + 1. +func (e *Element) NegateVal(val *Element, _ uint32) *Element { + // Since the element is already in the range 0 ≤ val < p, where p is the // secp256k1 prime, negation modulo p is just p - val. This implies that // the result will always be in the desired range with the sole exception of // 0 because p - 0 = p itself. // // The following handles that case in constant time by creating a mask that - // is all 0s in the case the value being negated is 0 and all 1s otherwise - // and then bitwise ands that mask with each word of the prime. + // is all 0s in the case the element being negated is 0 and all 1s otherwise + // and then bitwise ands that mask with each limb of the prime. // Subtract val from 0. borrow is set iff val != 0. // @@ -322,55 +319,56 @@ func (f *FieldVal64) NegateVal(val *FieldVal64, _ uint32) *FieldVal64 { // The upper limbs of the prime are all 1s, so there is no need to mask them // given they are equal to the mask for both cases. mask := -borrow - maskedPrime0 := field64Prime0 & mask + maskedPrime0 := fieldPrimeLimb0 & mask // Add 0 when val == 0 or p when val != 0. The result is either: // - // val == 0: f = 0 + 0 = 0 - // val != 0: f = -val + p = p - val + // val == 0: e = 0 + 0 = 0 + // val != 0: e = -val + p = p - val var carry uint64 - f.n[0], carry = bits.Add64(t0, maskedPrime0, 0) - f.n[1], carry = bits.Add64(t1, mask, carry) - f.n[2], carry = bits.Add64(t2, mask, carry) - f.n[3], _ = bits.Add64(t3, mask, carry) - return f + e.n[0], carry = bits.Add64(t0, maskedPrime0, 0) + e.n[1], carry = bits.Add64(t1, mask, carry) + e.n[2], carry = bits.Add64(t2, mask, carry) + e.n[3], _ = bits.Add64(t3, mask, carry) + return e } -// Negate negates the field value in constant time. The existing field value is -// modified. The ignored parameter exists to keep API parity with [FieldVal]. +// Negate negates the element in constant time. The existing element is +// modified. The ignored parameter exists to keep API parity with the field +// element implementations. // -// The field value is returned to support chaining. This enables syntax like: -// f.Negate().AddInt(1) so that f = -f + 1. -func (f *FieldVal64) Negate(_ uint32) *FieldVal64 { - return f.NegateVal(f, 0) +// The element is returned to support chaining. This enables syntax like: +// e.Negate().AddInt(1) so that e = -e + 1. +func (e *Element) Negate(_ uint32) *Element { + return e.NegateVal(e, 0) } -// AddInt adds the passed integer to the existing field value and stores the -// result in f in constant time. This is a convenience function since it is -// fairly common to perform some arithmetic with small native integers. +// AddInt adds the passed integer to the existing element and stores the result +// in e in constant time. This is a convenience function since it is fairly +// common to perform some arithmetic with small native integers. // -// The field value is returned to support chaining. This enables syntax like: -// f.AddInt(1).Add(f2) so that f = f + 1 + f2. -func (f *FieldVal64) AddInt(ui uint16) *FieldVal64 { - return f.Add(new(FieldVal64).SetInt(ui)) +// The element is returned to support chaining. This enables syntax like: +// e.AddInt(1).Add(e2) so that e = e + 1 + e2. +func (e *Element) AddInt(ui uint16) *Element { + return e.Add(new(Element).SetInt(ui)) } -// Add adds the passed value to the existing field value and stores the result -// in f in constant time. +// Add adds the passed element to the existing element and stores the result in +// e in constant time. // -// The field value is returned to support chaining. This enables syntax like: -// f.Add(f2).AddInt(1) so that f = f + f2 + 1. -func (f *FieldVal64) Add(val *FieldVal64) *FieldVal64 { - return f.Add2(f, val) +// The element is returned to support chaining. This enables syntax like: +// e.Add(e2).AddInt(1) so that e = e + e2 + 1. +func (e *Element) Add(val *Element) *Element { + return e.Add2(e, val) } -// Add2 adds the passed two field values together and stores the result in f in +// Add2 adds the passed two elements together and stores the result in e in // constant time. // -// The field value is returned to support chaining. This enables syntax like: -// f3.Add2(f, f2).AddInt(1) so that f3 = f + f2 + 1. -func (f *FieldVal64) Add2(a, b *FieldVal64) *FieldVal64 { - // Since both values are already in the range 0 ≤ val < p (where p is the +// The element is returned to support chaining. This enables syntax like: +// e3.Add2(e, e2).AddInt(1) so that e3 = e + e2 + 1. +func (e *Element) Add2(a, b *Element) *Element { + // Since both elements are already in the range 0 ≤ val < p (where p is the // secp256k1 prime), the maximum possible result is < 2p - 1. So a maximum // of one subtraction of p is required in the worst case. // @@ -391,115 +389,115 @@ func (f *FieldVal64) Add2(a, b *FieldVal64) *FieldVal64 { // // s = t - p = a+b - p var s0, s1, s2, s3, borrow uint64 - s0, borrow = bits.Sub64(t0, field64Prime0, 0) - s1, borrow = bits.Sub64(t1, field64Prime1, borrow) - s2, borrow = bits.Sub64(t2, field64Prime2, borrow) - s3, borrow = bits.Sub64(t3, field64Prime3, borrow) + s0, borrow = bits.Sub64(t0, fieldPrimeLimb0, 0) + s1, borrow = bits.Sub64(t1, fieldPrimeLimb1, borrow) + s2, borrow = bits.Sub64(t2, fieldPrimeLimb2, borrow) + s3, borrow = bits.Sub64(t3, fieldPrimeLimb3, borrow) // Constant-time select. // - // Set f = t = a+b only when there was no overflow and t < p (borrow set). - // Otherwise f = s = a+b - p. + // Set e = t = a+b only when there was no overflow and t < p (borrow set). + // Otherwise e = s = a+b - p. cond := (1 - overflow) & borrow - f.n[0] = arith.ConstantTimeSelect64(cond, t0, s0) - f.n[1] = arith.ConstantTimeSelect64(cond, t1, s1) - f.n[2] = arith.ConstantTimeSelect64(cond, t2, s2) - f.n[3] = arith.ConstantTimeSelect64(cond, t3, s3) - return f + e.n[0] = arith.ConstantTimeSelect64(cond, t0, s0) + e.n[1] = arith.ConstantTimeSelect64(cond, t1, s1) + e.n[2] = arith.ConstantTimeSelect64(cond, t2, s2) + e.n[3] = arith.ConstantTimeSelect64(cond, t3, s3) + return e } -// MulBy2 multiplies the field value by 2 and stores the result in f in constant +// MulBy2 multiplies the element by 2 and stores the result in e in constant // time. // // This method is optimized to provide a significant speed advantage over the -// more general [FieldVal64.MulInt]. +// more general [Element.MulInt]. // -// The field value is returned to support chaining. This enables syntax like: -// f.MulBy2().Add(f2) so that f = 2 * f + f2. -func (f *FieldVal64) MulBy2() *FieldVal64 { - return f.Add(f) +// The element is returned to support chaining. This enables syntax like: +// e.MulBy2().Add(e2) so that e = 2 * e + e2. +func (e *Element) MulBy2() *Element { + return e.Add(e) } -// MulBy3 multiplies the field value by 3 and stores the result in f in constant +// MulBy3 multiplies the element by 3 and stores the result in e in constant // time. // // This method is optimized to provide a significant speed advantage over the -// more general [FieldVal64.MulInt]. +// more general [Element.MulInt]. // -// The field value is returned to support chaining. This enables syntax like: -// f.MulBy3().Add(f2) so that f = 3 * f + f2. -func (f *FieldVal64) MulBy3() *FieldVal64 { - var orig FieldVal64 - orig.Set(f) - return f.MulBy2().Add(&orig) +// The element is returned to support chaining. This enables syntax like: +// e.MulBy3().Add(e2) so that e = 3 * e + e2. +func (e *Element) MulBy3() *Element { + var orig Element + orig.Set(e) + return e.MulBy2().Add(&orig) } -// MulBy4 multiplies the field value by 4 and stores the result in f in constant +// MulBy4 multiplies the element by 4 and stores the result in e in constant // time. // // This method is optimized to provide a significant speed advantage over the -// more general [FieldVal64.MulInt]. +// more general [Element.MulInt]. // -// The field value is returned to support chaining. This enables syntax like: -// f.MulBy4().Add(f2) so that f = 4 * f + f2. -func (f *FieldVal64) MulBy4() *FieldVal64 { - return f.MulBy2().MulBy2() +// The element is returned to support chaining. This enables syntax like: +// e.MulBy4().Add(e2) so that e = 4 * e + e2. +func (e *Element) MulBy4() *Element { + return e.MulBy2().MulBy2() } -// MulBy8 multiplies the field value by 8 and stores the result in f in constant +// MulBy8 multiplies the element by 8 and stores the result in e in constant // time. // // This method is optimized to provide a significant speed advantage over the -// more general [FieldVal64.MulInt]. +// more general [Element.MulInt]. // -// The field value is returned to support chaining. This enables syntax like: -// f.MulBy8().Add(f2) so that f = 8 * f + f2. -func (f *FieldVal64) MulBy8() *FieldVal64 { - return f.MulBy4().MulBy2() +// The element is returned to support chaining. This enables syntax like: +// e.MulBy8().Add(e2) so that e = 8 * e + e2. +func (e *Element) MulBy8() *Element { + return e.MulBy4().MulBy2() } -// MulInt multiplies the field value by the passed int and stores the result in -// f in constant time. +// MulInt multiplies the element by the passed int and stores the result in e in +// constant time. // // Callers should prefer using the faster specialized methods for multiplying by // 2, 3, 4, and 8, as they are commonly used in curve equations. // -// See [FieldVal64.MulBy2], [FieldVal64.MulBy3], [FieldVal64.MulBy4], and -// [FieldVal64.MulBy8] for the aforementioned optimized methods. +// See [Element.MulBy2], [Element.MulBy3], [Element.MulBy4], and +// [Element.MulBy8] for the aforementioned optimized methods. // -// The field value is returned to support chaining. This enables syntax like: -// f.MulInt(15).Add(f2) so that f = 15 * f + f2. -func (f *FieldVal64) MulInt(val uint8) *FieldVal64 { - return f.Mul(new(FieldVal64).SetInt(uint16(val))) +// The element is returned to support chaining. This enables syntax like: +// e.MulInt(15).Add(e2) so that e = 15 * e + e2. +func (e *Element) MulInt(val uint8) *Element { + return e.Mul(new(Element).SetInt(uint16(val))) } -// Mul multiplies the passed value to the existing field value and stores the -// result in f in constant time. +// Mul multiplies the passed element to the existing element and stores the +// result in e in constant time. // -// The field value is returned to support chaining. This enables syntax like: -// f.Mul(f2).AddInt(1) so that f = (f * f2) + 1. -func (f *FieldVal64) Mul(val *FieldVal64) *FieldVal64 { - return f.Mul2(f, val) +// The element is returned to support chaining. This enables syntax like: +// e.Mul(e2).AddInt(1) so that e = (e * e2) + 1. +func (e *Element) Mul(val *Element) *Element { + return e.Mul2(e, val) } -// Mul2 multiplies the passed two field values together and stores the result in -// f in constant time. +// Mul2 multiplies the passed two elements together and stores the result in e +// in constant time. // -// The field value is returned to support chaining. This enables syntax like: -// f3.Mul2(f, f2).AddInt(1) so that f3 = (f * f2) + 1. -func (f *FieldVal64) Mul2(a, b *FieldVal64) *FieldVal64 { - field64MulReduce(&f.n, &a.n, &b.n) - return f +// The element is returned to support chaining. This enables syntax like: +// e3.Mul2(e, e2).AddInt(1) so that e3 = (e * e2) + 1. +func (e *Element) Mul2(a, b *Element) *Element { + mulReduce(&e.n, &a.n, &b.n) + return e } -// SquareRootVal either calculates the square root of the passed value when it -// exists or the square root of the negation of the value when it does not exist -// and stores the result in f in constant time. The return flag is true when -// the calculated square root is for the passed value itself and false when it -// is for its negation. -func (f *FieldVal64) SquareRootVal(val *FieldVal64) bool { +// SquareRootVal either calculates the square root of the passed element when it +// exists or the square root of the negation of the element when it does not +// exist and stores the result in e in constant time. The return flag is true +// when the calculated square root is for the passed element itself and false +// when it is for its negation. +func (e *Element) SquareRootVal(val *Element) bool { // This uses the Tonelli-Shanks method for calculating the square root of - // the value when it exists. The key principles of the method follow. + // the element when it exists. The key principles of the method follow. // // Fermat's little theorem states that for a nonzero number 'a' and prime // 'p', a^(p-1) ≡ 1 (mod p). @@ -521,15 +519,15 @@ func (f *FieldVal64) SquareRootVal(val *FieldVal64) bool { // The Tonelli-Shanks method uses these facts along with factoring out // powers of two to solve a congruence that results in either the solution // when the square root exists or the square root of the negation of the - // value when it does not. In the case of primes that are ≡ 3 (mod 4), the - // possible solutions are r = ±a^((p+1)/4) (mod p). Therefore, either r^2 ≡ - // a (mod p) is true in which case ±r are the two solutions, or r^2 ≡ -a - // (mod p) in which case 'a' is a non-residue and there are no solutions. + // element when it does not. In the case of primes that are ≡ 3 (mod 4), + // the possible solutions are r = ±a^((p+1)/4) (mod p). Therefore, either + // r^2 ≡ a (mod p) is true in which case ±r are the two solutions, or r^2 ≡ + // -a (mod p) in which case 'a' is a non-residue and there are no solutions. // // The secp256k1 prime is ≡ 3 (mod 4), so this result applies. // // In other words, calculate a^((p+1)/4) and then square it and check it - // against the original value to determine if it is actually the square + // against the original element to determine if it is actually the square // root. // // In order to efficiently compute a^((p+1)/4), (p+1)/4 needs to be split @@ -565,7 +563,7 @@ func (f *FieldVal64) SquareRootVal(val *FieldVal64) bool { // => 2^1 2^[2] 2^3 2^6 2^9 2^11 2^[22] 2^44 2^88 2^176 2^220 2^[223] // // This has a cost of 254 field squarings and 13 field multiplications. - var a, a2, a3, a6, a9, a11, a22, a44, a88, a176, a220, a223 FieldVal64 + var a, a2, a3, a6, a9, a11, a22, a44, a88, a176, a220, a223 Element a.Set(val) a2.SquareVal(&a).Mul(&a) // a2 = a^(2^2 - 1) a3.SquareVal(&a2).Mul(&a) // a3 = a^(2^3 - 1) @@ -627,48 +625,48 @@ func (f *FieldVal64) SquareRootVal(val *FieldVal64) bool { a223.SquareVal(&a220).Square().Square() // a223 = a^(2^223 - 2^3) a223.Mul(&a3) // a223 = a^(2^223 - 1) - f.SquareVal(&a223).Square().Square().Square().Square() // f = a^(2^228 - 2^5) - f.Square().Square().Square().Square().Square() // f = a^(2^233 - 2^10) - f.Square().Square().Square().Square().Square() // f = a^(2^238 - 2^15) - f.Square().Square().Square().Square().Square() // f = a^(2^243 - 2^20) - f.Square().Square().Square() // f = a^(2^246 - 2^23) - f.Mul(&a22) // f = a^(2^246 - 2^22 - 1) - f.Square().Square().Square().Square().Square() // f = a^(2^251 - 2^27 - 2^5) - f.Square() // f = a^(2^252 - 2^28 - 2^6) - f.Mul(&a2) // f = a^(2^252 - 2^28 - 2^6 - 2^1 - 1) - f.Square().Square() // f = a^(2^254 - 2^30 - 244) = a^((p+1)/4) + e.SquareVal(&a223).Square().Square().Square().Square() // e = a^(2^228 - 2^5) + e.Square().Square().Square().Square().Square() // e = a^(2^233 - 2^10) + e.Square().Square().Square().Square().Square() // e = a^(2^238 - 2^15) + e.Square().Square().Square().Square().Square() // e = a^(2^243 - 2^20) + e.Square().Square().Square() // e = a^(2^246 - 2^23) + e.Mul(&a22) // e = a^(2^246 - 2^22 - 1) + e.Square().Square().Square().Square().Square() // e = a^(2^251 - 2^27 - 2^5) + e.Square() // e = a^(2^252 - 2^28 - 2^6) + e.Mul(&a2) // e = a^(2^252 - 2^28 - 2^6 - 2^1 - 1) + e.Square().Square() // e = a^(2^254 - 2^30 - 244) = a^((p+1)/4) // Verify the result is actually the square root by squaring it and checking - // against the original value. - var sqr FieldVal64 - return sqr.SquareVal(f).Equals(val) + // against the original element. + var sqr Element + return sqr.SquareVal(e).Equals(val) } -// Square squares the field value in constant time. The existing field value is +// Square squares the element in constant time. The existing element is // modified. // -// The field value is returned to support chaining. This enables syntax like: -// f.Square().Mul(f2) so that f = f^2 * f2. -func (f *FieldVal64) Square() *FieldVal64 { - return f.SquareVal(f) +// The element is returned to support chaining. This enables syntax like: +// e.Square().Mul(e2) so that e = e^2 * e2. +func (e *Element) Square() *Element { + return e.SquareVal(e) } -// SquareVal squares the passed value and stores the result in f in constant +// SquareVal squares the passed element and stores the result in e in constant // time. // -// The field value is returned to support chaining. This enables syntax like: -// f3.SquareVal(f).Mul(f) so that f3 = f^2 * f = f^3. -func (f *FieldVal64) SquareVal(val *FieldVal64) *FieldVal64 { - field64SquareReduce(&f.n, &val.n) - return f +// The element is returned to support chaining. This enables syntax like: +// e3.SquareVal(e).Mul(e) so that e3 = e^2 * e = e^3. +func (e *Element) SquareVal(val *Element) *Element { + squareReduce(&e.n, &val.n) + return e } -// Inverse finds the modular multiplicative inverse of the field value in -// constant time. The existing field value is modified. +// Inverse finds the modular multiplicative inverse of the element in constant +// time. The existing element is modified. // -// The field value is returned to support chaining. This enables syntax like: -// f.Inverse().Mul(f2) so that f = f^-1 * f2. -func (f *FieldVal64) Inverse() *FieldVal64 { +// The element is returned to support chaining. This enables syntax like: +// e.Inverse().Mul(e2) so that e = e^-1 * e2. +func (e *Element) Inverse() *Element { // Fermat's little theorem states that for a nonzero number 'a' and prime // 'p', a^(p-1) ≡ 1 (mod p). Multiplying both sides of the equation by the // multiplicative inverse a^-1 yields a^(p-2) ≡ a^-1 (mod p). Thus, a^(p-2) @@ -710,8 +708,8 @@ func (f *FieldVal64) Inverse() *FieldVal64 { // => 2^[1] 2^[2] 2^3 2^6 2^9 2^11 2^[22] 2^44 2^88 2^176 2^220 2^[223] // // This has a cost of 255 field squarings and 15 field multiplications. - var a, a2, a3, a6, a9, a11, a22, a44, a88, a176, a220, a223 FieldVal64 - a.Set(f) + var a, a2, a3, a6, a9, a11, a22, a44, a88, a176, a220, a223 Element + a.Set(e) a2.SquareVal(&a).Mul(&a) // a2 = a^(2^2 - 1) a3.SquareVal(&a2).Mul(&a) // a3 = a^(2^3 - 1) a6.SquareVal(&a3).Square().Square() // a6 = a^(2^6 - 2^3) @@ -772,24 +770,23 @@ func (f *FieldVal64) Inverse() *FieldVal64 { a223.SquareVal(&a220).Square().Square() // a223 = a^(2^223 - 2^3) a223.Mul(&a3) // a223 = a^(2^223 - 1) - f.SquareVal(&a223).Square().Square().Square().Square() // f = a^(2^228 - 2^5) - f.Square().Square().Square().Square().Square() // f = a^(2^233 - 2^10) - f.Square().Square().Square().Square().Square() // f = a^(2^238 - 2^15) - f.Square().Square().Square().Square().Square() // f = a^(2^243 - 2^20) - f.Square().Square().Square() // f = a^(2^246 - 2^23) - f.Mul(&a22) // f = a^(2^246 - 4194305) - f.Square().Square().Square().Square().Square() // f = a^(2^251 - 134217760) - f.Mul(&a) // f = a^(2^251 - 134217759) - f.Square().Square().Square() // f = a^(2^254 - 1073742072) - f.Mul(&a2) // f = a^(2^254 - 1073742069) - f.Square().Square() // f = a^(2^256 - 4294968276) - return f.Mul(&a) // f = a^(2^256 - 4294968275) = a^(p-2) -} - -// IsGtOrEqPrimeMinusOrder returns whether or not the field value is greater -// than or equal to the field prime minus the secp256k1 group order in constant -// time. -func (f *FieldVal64) IsGtOrEqPrimeMinusOrder() bool { + e.SquareVal(&a223).Square().Square().Square().Square() // e = a^(2^228 - 2^5) + e.Square().Square().Square().Square().Square() // e = a^(2^233 - 2^10) + e.Square().Square().Square().Square().Square() // e = a^(2^238 - 2^15) + e.Square().Square().Square().Square().Square() // e = a^(2^243 - 2^20) + e.Square().Square().Square() // e = a^(2^246 - 2^23) + e.Mul(&a22) // e = a^(2^246 - 4194305) + e.Square().Square().Square().Square().Square() // e = a^(2^251 - 134217760) + e.Mul(&a) // e = a^(2^251 - 134217759) + e.Square().Square().Square() // e = a^(2^254 - 1073742072) + e.Mul(&a2) // e = a^(2^254 - 1073742069) + e.Square().Square() // e = a^(2^256 - 4294968276) + return e.Mul(&a) // e = a^(2^256 - 4294968275) = a^(p-2) +} + +// IsGtOrEqPrimeMinusOrder returns whether or not the element is greater than or +// equal to the field prime minus the secp256k1 group order in constant time. +func (e *Element) IsGtOrEqPrimeMinusOrder() bool { // The secp256k1 prime is equivalent to 2^256 - 4294968273 and the group // order is 2^256 - 432420386565659656852420866394968145599. Thus, the // prime minus the group order is: @@ -798,7 +795,7 @@ func (f *FieldVal64) IsGtOrEqPrimeMinusOrder() bool { // In hex that is: // 0x00000000 00000000 00000000 00000001 45512319 50b75fc4 402da172 2fc9baee // - // Converting that to field representation (base 2^64) is: + // Converting that to the internal representation (base 2^64) is: // // n[0] = 0x402da1722fc9baee // n[1] = 0x4551231950b75fc4 @@ -807,28 +804,28 @@ func (f *FieldVal64) IsGtOrEqPrimeMinusOrder() bool { // // This can be verified with the following test code: // pMinusN := new(big.Int).Sub(curveParams.P, curveParams.N) - // var fv FieldVal64 - // fv.SetByteSlice(pMinusN.Bytes()) - // t.Logf("%x", fv.n) + // var v Element + // v.SetByteSlice(pMinusN.Bytes()) + // t.Logf("%x", v.n) // // Outputs: [402da1722fc9baee 4551231950b75fc4 1 0] const ( - field64PMinusN0 = 0x402da1722fc9baee - field64PMinusN1 = 0x4551231950b75fc4 - field64PMinusN2 = 0x0000000000000001 - field64PMinusN3 = 0x0000000000000000 + pMinusNLimb0 = 0x402da1722fc9baee + pMinusNLimb1 = 0x4551231950b75fc4 + pMinusNLimb2 = 0x0000000000000001 + pMinusNLimb3 = 0x0000000000000000 ) - // The goal is to return true when the value is greater than or equal to the - // field prime minus the group order. That is, return true when f ≥ p - n, - // which is trivially rearranged to f - (p - n) ≥ 0. + // The goal is to return true when the element is greater than or equal to + // the field prime minus the group order. That is, return true when e ≥ p - + // n, which is trivially rearranged to e - (p - n) ≥ 0. // - // In other words, the condition is met iff subtracting (p - n) from f is + // In other words, the condition is met iff subtracting (p - n) from e is // non-negative (aka there was no borrow). var borrow uint64 - _, borrow = bits.Sub64(f.n[0], field64PMinusN0, 0) - _, borrow = bits.Sub64(f.n[1], field64PMinusN1, borrow) - _, borrow = bits.Sub64(f.n[2], field64PMinusN2, borrow) - _, borrow = bits.Sub64(f.n[3], field64PMinusN3, borrow) + _, borrow = bits.Sub64(e.n[0], pMinusNLimb0, 0) + _, borrow = bits.Sub64(e.n[1], pMinusNLimb1, borrow) + _, borrow = bits.Sub64(e.n[2], pMinusNLimb2, borrow) + _, borrow = bits.Sub64(e.n[3], pMinusNLimb3, borrow) return borrow == 0 } diff --git a/dcrec/secp256k1/field4x64/field64_amd64.go b/dcrec/secp256k1/field4x64/element_amd64.go similarity index 57% rename from dcrec/secp256k1/field4x64/field64_amd64.go rename to dcrec/secp256k1/field4x64/element_amd64.go index c939d52723..108e77cb17 100644 --- a/dcrec/secp256k1/field4x64/field64_amd64.go +++ b/dcrec/secp256k1/field4x64/element_amd64.go @@ -10,15 +10,15 @@ import ( "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/cpufeat" ) -// field64UseBMI2AndADX is enabled when the CPU supports both BMI2 (MULX) and -// ADX (ADCX/ADOX). -var field64UseBMI2AndADX = func() bool { +// useBMI2AndADX is enabled when the CPU supports both BMI2 (MULX) and ADX +// (ADCX/ADOX). +var useBMI2AndADX = func() bool { f := cpufeat.Supported() return f.BMI2 && f.ADX }() //go:noescape -func field64MulReduceADX(r *[4]uint64, a, b *[4]uint64) +func mulReduceADX(r *[4]uint64, a, b *[4]uint64) //go:noescape -func field64SquareReduceADX(r *[4]uint64, a *[4]uint64) +func squareReduceADX(r *[4]uint64, a *[4]uint64) diff --git a/dcrec/secp256k1/field4x64/field64_amd64.s b/dcrec/secp256k1/field4x64/element_amd64.s similarity index 94% rename from dcrec/secp256k1/field4x64/field64_amd64.s rename to dcrec/secp256k1/field4x64/element_amd64.s index 4d5bbd7dd9..8b98035b72 100644 --- a/dcrec/secp256k1/field4x64/field64_amd64.s +++ b/dcrec/secp256k1/field4x64/element_amd64.s @@ -58,8 +58,8 @@ MOVQ R14, 16(AX) \ MOVQ R15, 24(AX) -// func field64MulReduceADX(r *[4]uint64, a, b *[4]uint64) -TEXT ·field64MulReduceADX(SB), NOSPLIT, $0-24 +// func mulReduceADX(r *[4]uint64, a, b *[4]uint64) +TEXT ·mulReduceADX(SB), NOSPLIT, $0-24 MOVQ a+8(FP), SI MOVQ b+16(FP), DI XORQ BX, BX @@ -126,8 +126,8 @@ TEXT ·field64MulReduceADX(SB), NOSPLIT, $0-24 REDUCE() RET -// func field64SquareReduceADX(r *[4]uint64, a *[4]uint64) -TEXT ·field64SquareReduceADX(SB), NOSPLIT, $0-16 +// func squareReduceADX(r *[4]uint64, a *[4]uint64) +TEXT ·squareReduceADX(SB), NOSPLIT, $0-16 MOVQ a+8(FP), AX MOVQ 0(AX), DX // a0 MOVQ 8(AX), SI // a1 diff --git a/dcrec/secp256k1/field4x64/field64_amd64_test.go b/dcrec/secp256k1/field4x64/element_amd64_test.go similarity index 84% rename from dcrec/secp256k1/field4x64/field64_amd64_test.go rename to dcrec/secp256k1/field4x64/element_amd64_test.go index 2e40804624..cfdffa7885 100644 --- a/dcrec/secp256k1/field4x64/field64_amd64_test.go +++ b/dcrec/secp256k1/field4x64/element_amd64_test.go @@ -6,12 +6,12 @@ package field4x64 import "testing" -// TestField64AMD64 ensures each of the specialized amd64 [FieldVal64.Mul] and -// [FieldVal64.Square] implementations return the expected results. +// TestElementAMD64 ensures each of the specialized amd64 [Element.Mul] and +// [Element.Square] implementations return the expected results. // // Note that any tests which require instruction sets that aren't available on // the system executing the tests are skipped. -func TestField64AMD64(t *testing.T) { +func TestElementAMD64(t *testing.T) { // NOTE: This is intentionally not made parallel because it modifies the // global feature flags to ensure all supported variants are tested. @@ -22,7 +22,7 @@ func TestField64AMD64(t *testing.T) { } variants := []mulVariantTest{ {name: "Pure Go", featureFlag: &pureGo}, - {name: "BMI2/ADX", featureFlag: &field64UseBMI2AndADX}, + {name: "BMI2/ADX", featureFlag: &useBMI2AndADX}, } // Restore the feature flags after the tests complete. @@ -56,10 +56,10 @@ func TestField64AMD64(t *testing.T) { *variant.featureFlag = true t.Run("Mul", func(t *testing.T) { - testField64Mul(t) + testElementMul(t) }) t.Run("Square", func(t *testing.T) { - testField64Square(t) + testElementSquare(t) }) }) } diff --git a/dcrec/secp256k1/field4x64/field64_bench_amd64_test.go b/dcrec/secp256k1/field4x64/element_bench_amd64_test.go similarity index 56% rename from dcrec/secp256k1/field4x64/field64_bench_amd64_test.go rename to dcrec/secp256k1/field4x64/element_bench_amd64_test.go index e02659fb20..f6c652e9f9 100644 --- a/dcrec/secp256k1/field4x64/field64_bench_amd64_test.go +++ b/dcrec/secp256k1/field4x64/element_bench_amd64_test.go @@ -10,22 +10,22 @@ import ( "testing" ) -// BenchmarkField64MulReduceAMD64 benchmarks how long it takes to multiply two -// field elements together and reduce them modulo the field prime with each of -// the specialized amd64 implementations along with the number of allocations +// BenchmarkMulReduceAMD64 benchmarks how long it takes to multiply two field +// elements together and reduce them modulo the field prime with each of the +// specialized amd64 implementations along with the number of allocations // needed. -func BenchmarkField64MulReduceAMD64(b *testing.B) { +func BenchmarkMulReduceAMD64(b *testing.B) { benches := []struct { name string fn func(r *[4]uint64, a, b *[4]uint64) supported bool }{ - {name: "Generic", fn: field64MulReduceGeneric, supported: true}, - {name: "BMI2/ADX", fn: field64MulReduceADX, supported: field64UseBMI2AndADX}, + {name: "Generic", fn: mulReduceGeneric, supported: true}, + {name: "BMI2/ADX", fn: mulReduceADX, supported: useBMI2AndADX}, } - a := mustFieldVal64("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab").n - c := mustFieldVal64("16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca").n + a := mustElement("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab").n + c := mustElement("16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca").n var r [4]uint64 for _, bench := range benches { @@ -47,21 +47,20 @@ func BenchmarkField64MulReduceAMD64(b *testing.B) { } } -// BenchmarkField64SquareReduceAMD64 benchmarks how long it takes to square a -// field element and reduce it modulo the field prime with each of the -// specialized amd64 implementations along with the number of allocations -// needed. -func BenchmarkField64SquareReduceAMD64(b *testing.B) { +// BenchmarkSquareReduceAMD64 benchmarks how long it takes to square a field +// element and reduce it modulo the field prime with each of the specialized +// amd64 implementations along with the number of allocations needed. +func BenchmarkSquareReduceAMD64(b *testing.B) { benches := []struct { name string fn func(r *[4]uint64, a *[4]uint64) supported bool }{ - {name: "Generic", fn: field64SquareReduceGeneric, supported: true}, - {name: "BMI2/ADX", fn: field64SquareReduceADX, supported: field64UseBMI2AndADX}, + {name: "Generic", fn: squareReduceGeneric, supported: true}, + {name: "BMI2/ADX", fn: squareReduceADX, supported: useBMI2AndADX}, } - a := mustFieldVal64("16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca").n + a := mustElement("16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca").n var r [4]uint64 for _, bench := range benches { diff --git a/dcrec/secp256k1/field4x64/element_bench_test.go b/dcrec/secp256k1/field4x64/element_bench_test.go new file mode 100644 index 0000000000..96579a079c --- /dev/null +++ b/dcrec/secp256k1/field4x64/element_bench_test.go @@ -0,0 +1,175 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package field4x64 + +import ( + "testing" +) + +// BenchmarkElementNegate benchmarks calculating the additive inverse of an +// unsigned 256-bit big-endian integer modulo the field prime with [Element]. +func BenchmarkElementNegate(b *testing.B) { + // The function is constant time so any value is fine. + valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(valHex) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + var result Element + _ = result.NegateVal(e, 0) + } +} + +// BenchmarkElementAdd benchmarks adding two unsigned 256-bit big-endian +// integers modulo the field prime with [Element]. +func BenchmarkElementAdd(b *testing.B) { + a := mustElement("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab") + c := mustElement("16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + var sum Element + sum.Add2(a, c) + } +} + +// BenchmarkElementMulBy2 benchmarks multiplying an unsigned 256-bit big-endian +// integer by 2 with [Element.MulBy2]. +func BenchmarkElementMulBy2(b *testing.B) { + eHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(eHex) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + e.MulBy2() + } +} + +// BenchmarkElementMulBy3 benchmarks multiplying an unsigned 256-bit big-endian +// integer by 3 with [Element.MulBy3]. +func BenchmarkElementMulBy3(b *testing.B) { + eHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(eHex) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + e.MulBy3() + } +} + +// BenchmarkElementMulBy4 benchmarks multiplying an unsigned 256-bit big-endian +// integer by 4 with [Element.MulBy4]. +func BenchmarkElementMulBy4(b *testing.B) { + eHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(eHex) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + e.MulBy4() + } +} + +// BenchmarkElementMulBy8 benchmarks multiplying an unsigned 256-bit big-endian +// integer by 8 with [Element.MulBy8]. +func BenchmarkElementMulBy8(b *testing.B) { + eHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(eHex) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + e.MulBy8() + } +} + +// BenchmarkFieldMulInt benchmarks multiplying an unsigned 256-bit big-endian +// integer by small integers with [Element.MulInt]. +func BenchmarkElementMulInt(b *testing.B) { + eHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(eHex) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + e.MulInt(2) + } +} + +// BenchmarkElementMul benchmarks multiplying two unsigned 256-bit big-endian +// integers modulo the field prime with [Element]. +func BenchmarkElementMul(b *testing.B) { + a := mustElement("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab") + c := mustElement("16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + var prod Element + prod.Mul2(a, c) + } +} + +// BenchmarkElementSqrt benchmarks calculating the square root of an unsigned +// 256-bit big-endian integer modulo the field prime with [Element]. +func BenchmarkElementSqrt(b *testing.B) { + // The function is constant time so any value is fine. + valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(valHex) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + var result Element + _ = result.SquareRootVal(e) + } +} + +// BenchmarkElementSquare benchmarks squaring a 256-bit big-endian integer +// modulo the field prime with [Element]. +func BenchmarkElementSquare(b *testing.B) { + a := mustElement("16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + var sq Element + sq.SquareVal(a) + } +} + +// BenchmarkElementInverse benchmarks calculating the multiplicative inverse of +// an unsigned 256-bit big-endian integer modulo the field prime with +// [Element]. +func BenchmarkElementInverse(b *testing.B) { + // The function is constant time so any value is fine. + valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(valHex) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + e.Inverse() + } +} + +// BenchmarkElementIsGtOrEqPrimeMinusOrder benchmarks determining whether a +// value is greater than or equal to the field prime minus the group order with +// [Element]. +func BenchmarkElementIsGtOrEqPrimeMinusOrder(b *testing.B) { + // The function is constant time so any value is fine. + valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" + e := mustElement(valHex) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = e.IsGtOrEqPrimeMinusOrder() + } +} diff --git a/dcrec/secp256k1/field4x64/field64_generic.go b/dcrec/secp256k1/field4x64/element_generic.go similarity index 75% rename from dcrec/secp256k1/field4x64/field64_generic.go rename to dcrec/secp256k1/field4x64/element_generic.go index faa45d28cd..d5b10840f2 100644 --- a/dcrec/secp256k1/field4x64/field64_generic.go +++ b/dcrec/secp256k1/field4x64/element_generic.go @@ -10,9 +10,9 @@ import ( "github.com/decred/dcrd/dcrec/secp256k1/v4/internal/arith" ) -// field64Reduce512 reduces a 512-bit little-endian limb array modulo p in -// constant time and stores the result in r using pure Go. -func field64Reduce512(r *[4]uint64, x *[8]uint64) { +// reduce512 reduces a 512-bit little-endian limb array modulo p in constant +// time and stores the result in r using pure Go. +func reduce512(r *[4]uint64, x *[8]uint64) { // This algorithm has been formally verified, including its intermediate // bounds, carry assumptions, and functional correctness. The verification // artifacts are available in internal/proofs. @@ -46,7 +46,7 @@ func field64Reduce512(r *[4]uint64, x *[8]uint64) { var t0, t1, t2, t3, t4, h, lo, hi, carry uint64 - h, t0 = bits.Mul64(x[4], field64PrimeComplement) + h, t0 = bits.Mul64(x[4], fieldPrimeComplement) // Note that since hi is the upper 64 bits of the product of a uint64 with // c and c < 2^33: @@ -57,15 +57,15 @@ func field64Reduce512(r *[4]uint64, x *[8]uint64) { // // Therefore, it is safe to discard the carry and the same applies to the // next two limbs (second h and first t4). - hi, lo = bits.Mul64(x[5], field64PrimeComplement) + hi, lo = bits.Mul64(x[5], fieldPrimeComplement) t1, carry = bits.Add64(lo, h, 0) h, _ = bits.Add64(hi, 0, carry) - hi, lo = bits.Mul64(x[6], field64PrimeComplement) + hi, lo = bits.Mul64(x[6], fieldPrimeComplement) t2, carry = bits.Add64(lo, h, 0) h, _ = bits.Add64(hi, 0, carry) - hi, lo = bits.Mul64(x[7], field64PrimeComplement) + hi, lo = bits.Mul64(x[7], fieldPrimeComplement) t3, carry = bits.Add64(lo, h, 0) t4, _ = bits.Add64(hi, 0, carry) @@ -81,7 +81,7 @@ func field64Reduce512(r *[4]uint64, x *[8]uint64) { // The value now fits in 289 bits, so reduce it again. Only the fifth limb // (t4) needs to be considered since all of the higher limbs are ≥ 320 bits // and thus guaranteed to be 0. - h, t4 = bits.Mul64(t4, field64PrimeComplement) + h, t4 = bits.Mul64(t4, fieldPrimeComplement) t0, carry = bits.Add64(t0, t4, 0) t1, carry = bits.Add64(t1, h, carry) @@ -94,10 +94,10 @@ func field64Reduce512(r *[4]uint64, x *[8]uint64) { t4 = carry var s0, s1, s2, s3, borrow uint64 - s0, borrow = bits.Sub64(t0, field64Prime0, 0) - s1, borrow = bits.Sub64(t1, field64Prime1, borrow) - s2, borrow = bits.Sub64(t2, field64Prime2, borrow) - s3, borrow = bits.Sub64(t3, field64Prime3, borrow) + s0, borrow = bits.Sub64(t0, fieldPrimeLimb0, 0) + s1, borrow = bits.Sub64(t1, fieldPrimeLimb1, borrow) + s2, borrow = bits.Sub64(t2, fieldPrimeLimb2, borrow) + s3, borrow = bits.Sub64(t3, fieldPrimeLimb3, borrow) _, borrow = bits.Sub64(t4, 0, borrow) r[0] = arith.ConstantTimeSelect64(borrow, t0, s0) r[1] = arith.ConstantTimeSelect64(borrow, t1, s1) @@ -105,20 +105,20 @@ func field64Reduce512(r *[4]uint64, x *[8]uint64) { r[3] = arith.ConstantTimeSelect64(borrow, t3, s3) } -// field64MulReduceGeneric sets r = a * b (mod p). This is a generic -// implementation that performs the multiplication and reduction steps -// separately without any reliance on specific hardware extensions. -func field64MulReduceGeneric(r *[4]uint64, a, b *[4]uint64) { +// mulReduceGeneric sets r = a * b (mod p). This is a generic implementation +// that performs the multiplication and reduction steps separately without any +// reliance on specific hardware extensions. +func mulReduceGeneric(r *[4]uint64, a, b *[4]uint64) { var product [8]uint64 arith.Mul512(&product, a, b) - field64Reduce512(r, &product) + reduce512(r, &product) } -// field64SquareReduceGeneric sets r = a^2 (mod p). This is a generic -// implementation that performs the squaring and reduction steps separately -// without any reliance on specific hardware extensions. -func field64SquareReduceGeneric(r *[4]uint64, a *[4]uint64) { +// squareReduceGeneric sets r = a^2 (mod p). This is a generic implementation +// that performs the squaring and reduction steps separately without any +// reliance on specific hardware extensions. +func squareReduceGeneric(r *[4]uint64, a *[4]uint64) { var product [8]uint64 arith.Square512(&product, a) - field64Reduce512(r, &product) + reduce512(r, &product) } diff --git a/dcrec/secp256k1/field4x64/element_noasm.go b/dcrec/secp256k1/field4x64/element_noasm.go new file mode 100644 index 0000000000..c94b4e8bfc --- /dev/null +++ b/dcrec/secp256k1/field4x64/element_noasm.go @@ -0,0 +1,23 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +//go:build !amd64 || purego + +package field4x64 + +// useBMI2AndADX depends on hardware support and access to assembly +// instructions. +var useBMI2AndADX = false + +// mulReduce sets r = a * b (mod p). This defers to the generic +// implementation that does not rely on the optimized assembly implementations. +func mulReduce(r *[4]uint64, a, b *[4]uint64) { + mulReduceGeneric(r, a, b) +} + +// squareReduce sets r = a^2 (mod p). This defers to the generic implementation +// that does not rely on the optimized assembly implementations. +func squareReduce(r *[4]uint64, a *[4]uint64) { + squareReduceGeneric(r, a) +} diff --git a/dcrec/secp256k1/field4x64/field64_test.go b/dcrec/secp256k1/field4x64/element_test.go similarity index 75% rename from dcrec/secp256k1/field4x64/field64_test.go rename to dcrec/secp256k1/field4x64/element_test.go index 052a29aeb2..4d18442339 100644 --- a/dcrec/secp256k1/field4x64/field64_test.go +++ b/dcrec/secp256k1/field4x64/element_test.go @@ -15,14 +15,14 @@ import ( "time" ) -// mustFieldVal64Internal converts the passed hex string into a [FieldVal64] and -// will panic if there is an error. Values that overflow are treated as an -// error unless the allow overflow flag is set. +// mustElementInternal converts the passed hex string into an [Element] and will +// panic if there is an error. Values that overflow are treated as an error +// unless the allow overflow flag is set. // // This is only provided for the hard-coded constants so errors in the source // code can be detected. It will only (and must only) be called with hard-coded // values. -func mustFieldVal64Internal(s string, allowOverflow bool) *FieldVal64 { +func mustElementInternal(s string, allowOverflow bool) *Element { if len(s)%2 != 0 { s = "0" + s } @@ -33,37 +33,37 @@ func mustFieldVal64Internal(s string, allowOverflow bool) *FieldVal64 { if len(b) > 32 { panic("hex in source file overflows uint256: " + s) } - var f FieldVal64 + var f Element if overflow := f.SetByteSlice(b); overflow && !allowOverflow { - panic("hex in source file overflows mod N scalar: " + s) + panic("hex in source file overflows field prime: " + s) } return &f } -// mustFieldVal64 converts the passed hex string into a [FieldVal64] and will -// panic if there is an error. Values that overflow are treated as an error. +// mustElement converts the passed hex string into an [Element] and will panic if +// there is an error. Values that overflow are treated as an error. // // This is only provided for the hard-coded constants so errors in the source // code can be detected. It will only (and must only) be called with hard-coded // values. -func mustFieldVal64(s string) *FieldVal64 { - return mustFieldVal64Internal(s, false) +func mustElement(s string) *Element { + return mustElementInternal(s, false) } -// mustFieldVal64WithOverflow converts the passed hex string into a [FieldVal64] -// and will panic if there is an error. Values that overflow are NOT treated as -// an error. +// mustElementWithOverflow converts the passed hex string into an [Element] and +// will panic if there is an error. Values that overflow are NOT treated as an +// error. // // This is only provided for the hard-coded constants so errors in the source // code can be detected. It will only (and must only) be called with hard-coded // values. -func mustFieldVal64WithOverflow(s string) *FieldVal64 { - return mustFieldVal64Internal(s, true) +func mustElementWithOverflow(s string) *Element { + return mustElementInternal(s, true) } -// randIntAndFieldVal64 returns a big integer and a field value both created from -// the same random value generated by the passed rng. -func randIntAndFieldVal64(t *testing.T, rng *rand.Rand) (*big.Int, *FieldVal64) { +// randIntAndElement returns a [big.Int] and [Element] both created from the +// same random value generated by the passed rng. +func randIntAndElement(t *testing.T, rng *rand.Rand) (*big.Int, *Element) { t.Helper() var buf [32]byte @@ -73,14 +73,14 @@ func randIntAndFieldVal64(t *testing.T, rng *rand.Rand) (*big.Int, *FieldVal64) bigIntVal := new(big.Int).SetBytes(buf[:]) bigIntVal.Mod(bigIntVal, curveParams.P) - var fv FieldVal64 + var fv Element fv.SetBytes(&buf) return bigIntVal, &fv } -// TestField64SetInt ensures that setting a field value to various native -// integers works as expected. -func TestField64SetInt(t *testing.T) { +// TestElementSetInt ensures that setting an element to various native integers +// works as expected. +func TestElementSetInt(t *testing.T) { tests := []struct { name string // test description in uint16 // test value @@ -91,21 +91,21 @@ func TestField64SetInt(t *testing.T) { } for _, test := range tests { - f := new(FieldVal64).SetInt(test.in) + e := new(Element).SetInt(test.in) var want [32]byte binary.BigEndian.PutUint16(want[30:], test.in) - if !bytes.Equal(f.Bytes()[:], want[:]) { + if !bytes.Equal(e.Bytes()[:], want[:]) { t.Errorf("%s: wrong result\ngot: %x\nwant: %x", test.name, - f.Bytes()[:], want[:]) + e.Bytes()[:], want[:]) continue } } } -// TestField64SetBytes ensures that setting a field value to a 256-bit big-endian +// TestElementSetBytes ensures that setting an element to a 256-bit big-endian // unsigned integer via both the slice and array methods works as expected for // edge cases. Random cases are tested via the various other tests. -func TestField64SetBytes(t *testing.T) { +func TestElementSetBytes(t *testing.T) { tests := []struct { name string // test description in string // hex encoded test value @@ -183,7 +183,7 @@ func TestField64SetBytes(t *testing.T) { for _, test := range tests { inBytes := hexToBytes(test.in) - // FieldVal64 only consumes the first 32 bytes and always reduces, so + // [Element] only consumes the first 32 bytes and always reduces, so // derive the expected reduced value from the truncated input. truncated := inBytes if len(truncated) > 32 { @@ -195,7 +195,7 @@ func TestField64SetBytes(t *testing.T) { wantInt.FillBytes(wantBytes[:]) // Ensure setting the bytes via the slice method works as expected. - var f FieldVal64 + var f Element overflow := f.SetByteSlice(inBytes) if !bytes.Equal(f.Bytes()[:], wantBytes[:]) { t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, @@ -209,7 +209,7 @@ func TestField64SetBytes(t *testing.T) { } // Ensure setting the bytes via the array method works as expected. - var f2 FieldVal64 + var f2 Element var b32 [32]byte copy(b32[32-len(truncated):], truncated) overflow = f2.SetBytes(&b32) != 0 @@ -226,10 +226,10 @@ func TestField64SetBytes(t *testing.T) { } } -// TestField64Bytes ensures that retrieving the bytes for a 256-bit big-endian +// TestElementBytes ensures that retrieving the bytes for a 256-bit big-endian // unsigned integer via the various methods works as expected for edge cases. // Random cases are tested via the various other tests. -func TestField64Bytes(t *testing.T) { +func TestElementBytes(t *testing.T) { tests := []struct { name string // test description in string // hex encoded test value @@ -301,11 +301,11 @@ func TestField64Bytes(t *testing.T) { }} for _, test := range tests { - f := mustFieldVal64WithOverflow(test.in) + e := mustElementWithOverflow(test.in) expected := hexToBytes(test.expected) // Ensure getting the bytes works as expected. - gotBytes := f.Bytes() + gotBytes := e.Bytes() if !bytes.Equal(gotBytes[:], expected) { t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, *gotBytes, expected) @@ -314,7 +314,7 @@ func TestField64Bytes(t *testing.T) { // Ensure getting the bytes directly into an array works as expected. var b32 [32]byte - f.PutBytes(&b32) + e.PutBytes(&b32) if !bytes.Equal(b32[:], expected) { t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, b32, expected) @@ -323,7 +323,7 @@ func TestField64Bytes(t *testing.T) { // Ensure getting the bytes directly into a slice works as expected. var buffer [64]byte - f.PutBytesUnchecked(buffer[:]) + e.PutBytesUnchecked(buffer[:]) if !bytes.Equal(buffer[:32], expected) { t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, buffer[:32], expected) @@ -332,57 +332,57 @@ func TestField64Bytes(t *testing.T) { } } -// TestField64Zero ensures that zeroing a field value works as expected. -func TestField64Zero(t *testing.T) { - f := mustFieldVal64("a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5") - f.Zero() - for idx, limb := range f.n { +// TestElementZero ensures that zeroing an element works as expected. +func TestElementZero(t *testing.T) { + e := mustElement("a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5") + e.Zero() + for idx, limb := range e.n { if limb != 0 { t.Errorf("limb at index #%d is not zero - got %d", idx, limb) } } } -// TestField64IsZero ensures that checking if a field is zero via IsZero and -// IsZeroBit works as expected. -func TestField64IsZero(t *testing.T) { - f := new(FieldVal64) - if !f.IsZero() { - t.Errorf("new field value is not zero - got %v (rawints %x)", f, f.n) +// TestElementIsZero ensures that checking if an [Element] is zero via +// [Element.IsZero] and [Element.IsZeroBit] works as expected. +func TestElementIsZero(t *testing.T) { + e := new(Element) + if !e.IsZero() { + t.Errorf("new element is not zero - got %v (rawints %x)", e, e.n) } - if f.IsZeroBit() != 1 { - t.Errorf("new field value is not zero - got %v (rawints %x)", f, f.n) + if e.IsZeroBit() != 1 { + t.Errorf("new element is not zero - got %v (rawints %x)", e, e.n) } - f.SetInt(1) - if f.IsZero() { - t.Errorf("claims zero for nonzero field - got %v (rawints %x)", f, f.n) + e.SetInt(1) + if e.IsZero() { + t.Errorf("claims zero for nonzero element - got %v (rawints %x)", e, e.n) } - if f.IsZeroBit() == 1 { - t.Errorf("claims zero for nonzero field - got %v (rawints %x)", f, f.n) + if e.IsZeroBit() == 1 { + t.Errorf("claims zero for nonzero element - got %v (rawints %x)", e, e.n) } - f.Zero() - if !f.IsZero() { - t.Errorf("claims nonzero for zero field - got %v (rawints %x)", f, f.n) + e.Zero() + if !e.IsZero() { + t.Errorf("claims nonzero for zero element - got %v (rawints %x)", e, e.n) } - if f.IsZeroBit() != 1 { - t.Errorf("claims nonzero for zero field - got %v (rawints %x)", f, f.n) + if e.IsZeroBit() != 1 { + t.Errorf("claims nonzero for zero element - got %v (rawints %x)", e, e.n) } - f.SetInt(1) - f.Zero() - if !f.IsZero() { - t.Errorf("claims zero for nonzero field - got %v (rawints %x)", f, f.n) + e.SetInt(1) + e.Zero() + if !e.IsZero() { + t.Errorf("claims zero for nonzero element - got %v (rawints %x)", e, e.n) } - if f.IsZeroBit() != 1 { - t.Errorf("claims zero for nonzero field - got %v (rawints %x)", f, f.n) + if e.IsZeroBit() != 1 { + t.Errorf("claims zero for nonzero element - got %v (rawints %x)", e, e.n) } } -// TestField64IsOne ensures that checking if a field is one via IsOne and IsOneBit -// works as expected. -func TestField64IsOne(t *testing.T) { +// TestElementIsOne ensures that checking if an [Element] is one via +// [Element.IsOne] and [Element.IsOneBit] works as expected. +func TestElementIsOne(t *testing.T) { tests := []struct { name string // test description in string // hex encoded test value @@ -422,15 +422,15 @@ func TestField64IsOne(t *testing.T) { }} for _, test := range tests { - f := mustFieldVal64WithOverflow(test.in) - result := f.IsOne() + e := mustElementWithOverflow(test.in) + result := e.IsOne() if result != test.expected { t.Errorf("%s: wrong result -- got: %v, want: %v", test.name, result, test.expected) continue } - result2 := f.IsOneBit() == 1 + result2 := e.IsOneBit() == 1 if result2 != test.expected { t.Errorf("%s: wrong result -- got: %v, want: %v", test.name, result2, test.expected) @@ -439,10 +439,104 @@ func TestField64IsOne(t *testing.T) { } } -// TestField64IsOdd ensures that checking if a field value is odd via IsOdd and -// IsOddBit works as expected. Unlike FieldVal, FieldVal64 is always fully -// reduced, so the parity is that of the reduced value. -func TestField64IsOdd(t *testing.T) { +// TestElementStringer ensures the stringer returns the appropriate hex string. +func TestElementStringer(t *testing.T) { + tests := []struct { + name string // test description + in string // hex encoded test value + expected string // expected result + }{{ + name: "zero", + in: "0", + expected: "0000000000000000000000000000000000000000000000000000000000000000", + }, { + name: "one", + in: "1", + expected: "0000000000000000000000000000000000000000000000000000000000000001", + }, { + name: "ten", + in: "a", + expected: "000000000000000000000000000000000000000000000000000000000000000a", + }, { + name: "eleven", + in: "b", + expected: "000000000000000000000000000000000000000000000000000000000000000b", + }, { + name: "twelve", + in: "c", + expected: "000000000000000000000000000000000000000000000000000000000000000c", + }, { + name: "thirteen", + in: "d", + expected: "000000000000000000000000000000000000000000000000000000000000000d", + }, { + name: "fourteen", + in: "e", + expected: "000000000000000000000000000000000000000000000000000000000000000e", + }, { + name: "fifteen", + in: "f", + expected: "000000000000000000000000000000000000000000000000000000000000000f", + }, { + name: "240", + in: "f0", + expected: "00000000000000000000000000000000000000000000000000000000000000f0", + }, { + name: "2^26 - 1", + in: "3ffffff", + expected: "0000000000000000000000000000000000000000000000000000000003ffffff", + }, { + name: "2^32 - 1", + in: "ffffffff", + expected: "00000000000000000000000000000000000000000000000000000000ffffffff", + }, { + name: "2^64 - 1", + in: "ffffffffffffffff", + expected: "000000000000000000000000000000000000000000000000ffffffffffffffff", + }, { + name: "2^96 - 1", + in: "ffffffffffffffffffffffff", + expected: "0000000000000000000000000000000000000000ffffffffffffffffffffffff", + }, { + name: "2^128 - 1", + in: "ffffffffffffffffffffffffffffffff", + expected: "00000000000000000000000000000000ffffffffffffffffffffffffffffffff", + }, { + name: "2^160 - 1", + in: "ffffffffffffffffffffffffffffffffffffffff", + expected: "000000000000000000000000ffffffffffffffffffffffffffffffffffffffff", + }, { + name: "2^192 - 1", + in: "ffffffffffffffffffffffffffffffffffffffffffffffff", + expected: "0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffff", + }, { + name: "2^224 - 1", + in: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + expected: "00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, { + name: "2^256-4294968273 (the secp256k1 prime, so should result in 0)", + in: "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", + expected: "0000000000000000000000000000000000000000000000000000000000000000", + }, { + name: "2^256-4294968274 (the secp256k1 prime+1, so should result in 1)", + in: "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc30", + expected: "0000000000000000000000000000000000000000000000000000000000000001", + }} + + for _, test := range tests { + e := mustElementWithOverflow(test.in) + result := e.String() + if result != test.expected { + t.Errorf("%s: wrong result\ngot: %v\nwant: %v", test.name, result, + test.expected) + continue + } + } +} + +// TestElementIsOdd ensures that checking if an element is odd via +// [Element.IsOdd] and [Element.IsOddBit] works as expected. +func TestElementIsOdd(t *testing.T) { tests := []struct { name string // test description in string // hex encoded value @@ -478,15 +572,15 @@ func TestField64IsOdd(t *testing.T) { }} for _, test := range tests { - f := mustFieldVal64WithOverflow(test.in) - result := f.IsOdd() + e := mustElementWithOverflow(test.in) + result := e.IsOdd() if result != test.expected { t.Errorf("%s: wrong result -- got: %v, want: %v", test.name, result, test.expected) continue } - result2 := f.IsOddBit() == 1 + result2 := e.IsOddBit() == 1 if result2 != test.expected { t.Errorf("%s: wrong result -- got: %v, want: %v", test.name, result2, test.expected) @@ -495,9 +589,9 @@ func TestField64IsOdd(t *testing.T) { } } -// TestField64Equals ensures that checking two field values for equality via -// Equals works as expected. -func TestField64Equals(t *testing.T) { +// TestElementEquals ensures that checking two elements for equality via +// [Element.Equals] works as expected. +func TestElementEquals(t *testing.T) { tests := []struct { name string // test description in1 string // hex encoded value @@ -541,9 +635,9 @@ func TestField64Equals(t *testing.T) { }} for _, test := range tests { - f := mustFieldVal64WithOverflow(test.in1) - f2 := mustFieldVal64WithOverflow(test.in2) - result := f.Equals(f2) + e := mustElementWithOverflow(test.in1) + e2 := mustElementWithOverflow(test.in2) + result := e.Equals(e2) if result != test.expected { t.Errorf("%s: wrong result -- got: %v, want: %v", test.name, result, test.expected) @@ -552,9 +646,9 @@ func TestField64Equals(t *testing.T) { } } -// TestField64Negate ensures that negating field values via Negate works as -// expected. -func TestField64Negate(t *testing.T) { +// TestElementNegate ensures that negating elements via [Element.Negate] works +// as expected. +func TestElementNegate(t *testing.T) { tests := []struct { name string // test description in string // hex encoded test value @@ -602,11 +696,11 @@ func TestField64Negate(t *testing.T) { }} for _, test := range tests { - f := mustFieldVal64WithOverflow(test.in) - expected := mustFieldVal64(test.expected) + e := mustElementWithOverflow(test.in) + expected := mustElement(test.expected) // Ensure negating another value produces the expected result. - result := new(FieldVal64).NegateVal(f, 0) + result := new(Element).NegateVal(e, 0) if !result.Equals(expected) { t.Errorf("%s: unexpected result -- got: %x, want: %x", test.name, result.Bytes(), expected.Bytes()) @@ -614,7 +708,7 @@ func TestField64Negate(t *testing.T) { } // Ensure self negating also produces the expected result. - result2 := f.Negate(0) + result2 := e.Negate(0) if !result2.Equals(expected) { t.Errorf("%s: unexpected result -- got: %x, want: %x", test.name, result2.Bytes(), expected.Bytes()) @@ -623,9 +717,9 @@ func TestField64Negate(t *testing.T) { } } -// TestField64AddInt ensures that adding an integer to field values via AddInt -// works as expected. -func TestField64AddInt(t *testing.T) { +// TestElementAddInt ensures that adding an integer to elements via +// [Element.AddInt] works as expected. +func TestElementAddInt(t *testing.T) { tests := []struct { name string // test description in1 string // hex encoded value @@ -679,9 +773,9 @@ func TestField64AddInt(t *testing.T) { }} for _, test := range tests { - f := mustFieldVal64WithOverflow(test.in1) - expected := mustFieldVal64(test.expected) - result := f.AddInt(test.in2) + e := mustElementWithOverflow(test.in1) + expected := mustElement(test.expected) + result := e.AddInt(test.in2) if !result.Equals(expected) { t.Errorf("%s: wrong result -- got: %x -- want: %x", test.name, result.Bytes(), expected.Bytes()) @@ -690,9 +784,9 @@ func TestField64AddInt(t *testing.T) { } } -// TestField64Add ensures that adding two field values together via Add and Add2 -// works as expected. -func TestField64Add(t *testing.T) { +// TestElementAdd ensures that adding two elements together via [Element.Add] +// and [Element.Add2] works as expected. +func TestElementAdd(t *testing.T) { tests := []struct { name string // test description in1 string // first hex encoded value @@ -761,21 +855,21 @@ func TestField64Add(t *testing.T) { }} for _, test := range tests { - f1 := mustFieldVal64WithOverflow(test.in1) - f2 := mustFieldVal64WithOverflow(test.in2) - expected := mustFieldVal64(test.expected) + f1 := mustElementWithOverflow(test.in1) + f2 := mustElementWithOverflow(test.in2) + expected := mustElement(test.expected) // Ensure adding the two values with the result going to another // variable produces the expected result. - result := new(FieldVal64).Add2(f1, f2) + result := new(Element).Add2(f1, f2) if !result.Equals(expected) { t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, result.Bytes(), expected.Bytes()) continue } - // Ensure adding the value to an existing field value produces the - // expected result. + // Ensure adding the value to an existing element produces the expected + // result. f1.Add(f2) if !f1.Equals(expected) { t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, @@ -785,18 +879,18 @@ func TestField64Add(t *testing.T) { } } -// TestField64MulByX ensures the dedicated small-constant multiply helpers +// TestElementMulByX ensures the dedicated small-constant multiply helpers // (MulBy2, MulBy3, MulBy4, and MulBy8) produce the same results as the // equivalent MulInt call. -func TestField64MulByX(t *testing.T) { +func TestElementMulByX(t *testing.T) { mulByFuncs := []struct { factor uint8 - fn func(*FieldVal64) *FieldVal64 + fn func(*Element) *Element }{ - {2, (*FieldVal64).MulBy2}, - {3, (*FieldVal64).MulBy3}, - {4, (*FieldVal64).MulBy4}, - {8, (*FieldVal64).MulBy8}, + {2, (*Element).MulBy2}, + {3, (*Element).MulBy3}, + {4, (*Element).MulBy4}, + {8, (*Element).MulBy8}, } testCases := []string{ @@ -814,12 +908,12 @@ func TestField64MulByX(t *testing.T) { "e3cbe002cc93029190181a906ed41af401c9726546dc19389a06290efdf563f1", // random sampling } for _, in := range testCases { - in := mustFieldVal64(in) + in := mustElement(in) for _, m := range mulByFuncs { - var want FieldVal64 + var want Element want.Set(in).MulInt(m.factor) - var f FieldVal64 + var f Element got := m.fn(f.Set(in)) if !got.Equals(&want) { t.Errorf("MulBy%d(%x): wrong result -- got: %x -- want: %x", @@ -829,9 +923,9 @@ func TestField64MulByX(t *testing.T) { } } -// TestField64MulInt ensures that multiplying an integer to field values via -// MulInt works as expected. -func TestField64MulInt(t *testing.T) { +// TestElementMulInt ensures that multiplying an integer to elements via +// [Element.MulInt] works as expected. +func TestElementMulInt(t *testing.T) { tests := []struct { name string // test description in1 string // hex encoded value @@ -895,9 +989,9 @@ func TestField64MulInt(t *testing.T) { }} for _, test := range tests { - f := mustFieldVal64WithOverflow(test.in1) - expected := mustFieldVal64(test.expected) - result := f.MulInt(test.in2) + e := mustElementWithOverflow(test.in1) + expected := mustElement(test.expected) + result := e.MulInt(test.in2) if !result.Equals(expected) { t.Errorf("%s: wrong result -- got: %x -- want: %x", test.name, result.Bytes(), expected.Bytes()) @@ -906,10 +1000,10 @@ func TestField64MulInt(t *testing.T) { } } -// testField64Mul ensures that multiplying two field values via [Field64Val.Mul] -// and [Field64Val.Mul2] works as expected. This unexported version exists so -// it can be invoked with different run-time achitecture variants. -func testField64Mul(t *testing.T) { +// testElementMul ensures that multiplying two elements via [Element.Mul] and +// [Element.Mul2] works as expected. This unexported version exists so it can +// be invoked with different run-time achitecture variants. +func testElementMul(t *testing.T) { t.Helper() tests := []struct { @@ -985,20 +1079,20 @@ func testField64Mul(t *testing.T) { }} for _, test := range tests { - f1 := mustFieldVal64WithOverflow(test.in1) - f2 := mustFieldVal64WithOverflow(test.in2) - expected := mustFieldVal64(test.expected) + f1 := mustElementWithOverflow(test.in1) + f2 := mustElementWithOverflow(test.in2) + expected := mustElement(test.expected) // Ensure multiplying the two values with the result going to another // variable produces the expected result. - result := new(FieldVal64).Mul2(f1, f2) + result := new(Element).Mul2(f1, f2) if !result.Equals(expected) { t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, result.Bytes(), expected.Bytes()) continue } - // Ensure multiplying the value to an existing field value produces the + // Ensure multiplying the value to an existing element produces the // expected result. f1.Mul(f2) if !f1.Equals(expected) { @@ -1009,16 +1103,16 @@ func testField64Mul(t *testing.T) { } } -// TestField64Mul ensures that multiplying two field values via [Field64Val.Mul] -// and [Field64Val.Mul2] works as expected. -func TestField64Mul(t *testing.T) { - testField64Mul(t) +// TestElementMul ensures that multiplying two elements via [Element.Mul] and +// [Element.Mul2] works as expected. +func TestElementMul(t *testing.T) { + testElementMul(t) } -// testField64Square ensures that squaring field values via [Field64Val.Square] -// and [Field64.SquareVal] works as expected. This unexported version exists so -// it can be invoked with different run-time achitecture variants. -func testField64Square(t *testing.T) { +// testElementSquare ensures that squaring elements via [Element.Square] and +// [Element.Square] works as expected. This unexported version exists so it can +// be invoked with different run-time achitecture variants. +func testElementSquare(t *testing.T) { t.Helper() tests := []struct { @@ -1060,38 +1154,38 @@ func testField64Square(t *testing.T) { }} for _, test := range tests { - f := mustFieldVal64WithOverflow(test.in) - expected := mustFieldVal64(test.expected) + e := mustElementWithOverflow(test.in) + expected := mustElement(test.expected) // Ensure squaring the value with the result going to another variable // produces the expected result. - result := new(FieldVal64).SquareVal(f) + result := new(Element).SquareVal(e) if !result.Equals(expected) { t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, result.Bytes(), expected.Bytes()) continue } - // Ensure self squaring an existing field value produces the expected + // Ensure self squaring an existing element produces the expected // result. - f.Square() - if !f.Equals(expected) { + e.Square() + if !e.Equals(expected) { t.Errorf("%s: unexpected result\ngot: %x\nwant: %x", test.name, - f.Bytes(), expected.Bytes()) + e.Bytes(), expected.Bytes()) continue } } } -// TestField64Square ensures that squaring field values via [Field64Val.Square] -// and [Field64.SquareVal] works as expected. -func TestField64Square(t *testing.T) { - testField64Square(t) +// TestElementSquare ensures that squaring elements via [Element.Square] and +// [Element.SquareVal] works as expected. +func TestElementSquare(t *testing.T) { + testElementSquare(t) } -// TestField64SquareRoot ensures that calculating the square root of field values -// via SquareRootVal works as expected for edge cases. -func TestField64SquareRoot(t *testing.T) { +// TestElementSquareRoot ensures that calculating the square root of elements +// via [Element.SquareRootVal] works as expected for edge cases. +func TestElementSquareRoot(t *testing.T) { tests := []struct { name string // test description in string // hex encoded value @@ -1160,12 +1254,12 @@ func TestField64SquareRoot(t *testing.T) { }} for _, test := range tests { - input := mustFieldVal64WithOverflow(test.in) - want := mustFieldVal64(test.want) + input := mustElementWithOverflow(test.in) + want := mustElement(test.want) // Calculate the square root and ensure the validity flag matches the // expected value. - var result FieldVal64 + var result Element isValid := result.SquareRootVal(input) if isValid != test.valid { t.Errorf("%s: mismatched validity -- got %v, want %v", test.name, @@ -1182,10 +1276,10 @@ func TestField64SquareRoot(t *testing.T) { } } -// TestField64SquareRootRandom ensures that calculating the square root for -// random field values works as expected by also performing the same operation -// with big ints and comparing the results. -func TestField64SquareRootRandom(t *testing.T) { +// TestElementSquareRootRandom ensures that calculating the square root for +// random elements works as expected by also performing the same operation with +// big ints and comparing the results. +func TestElementSquareRootRandom(t *testing.T) { // Use a unique random seed each test instance and log it if the tests fail. seed := time.Now().Unix() rng := rand.New(rand.NewSource(seed)) @@ -1196,39 +1290,39 @@ func TestField64SquareRootRandom(t *testing.T) { }(t, seed) for i := 0; i < 100; i++ { - // Generate big integer and field value with the same random value. - bigIntVal, fVal := randIntAndFieldVal64(t, rng) + // Generate big integer and element with the same random value. + bigIntVal, fVal := randIntAndElement(t, rng) // Calculate the square root of the value using big ints. bigIntResult := new(big.Int).ModSqrt(bigIntVal, curveParams.P) bigIntHasSqrt := bigIntResult != nil - // Calculate the square root of the value using a field value. - var fValResult FieldVal64 + // Calculate the square root of the value using an element. + var fValResult Element fValHasSqrt := fValResult.SquareRootVal(fVal) // Ensure they match. if bigIntHasSqrt != fValHasSqrt { - t.Fatalf("mismatched square root existence\nbig int in: %x\nfield "+ - "in: %x\nbig int result: %v\nfield result %v", bigIntVal, + t.Fatalf("mismatched square root existence\nbig int in: %x\nelem "+ + "in: %x\nbig int result: %v\nelem result %v", bigIntVal, fVal.Bytes(), bigIntHasSqrt, fValHasSqrt) } if !fValHasSqrt { continue } bigIntResultHex := fmt.Sprintf("%064x", bigIntResult) - fieldValResultHex := hex.EncodeToString(fValResult.Bytes()[:]) - if bigIntResultHex != fieldValResultHex { - t.Fatalf("mismatched square root\nbig int in: %x\nfield in: %x\n"+ - "big int result: %x\nfield result %x", bigIntVal, fVal.Bytes(), + elemResultHex := hex.EncodeToString(fValResult.Bytes()[:]) + if bigIntResultHex != elemResultHex { + t.Fatalf("mismatched square root\nbig int in: %x\nelem in: %x\n"+ + "big int result: %x\nelem result %x", bigIntVal, fVal.Bytes(), bigIntResult, fValResult.Bytes()) } } } -// TestField64Inverse ensures that finding the multiplicative inverse via Inverse -// works as expected. -func TestField64Inverse(t *testing.T) { +// TestElementInverse ensures that finding the multiplicative inverse via +// [Element.Inverse] works as expected. +func TestElementInverse(t *testing.T) { tests := []struct { name string // test description in string // hex encoded value @@ -1268,9 +1362,9 @@ func TestField64Inverse(t *testing.T) { }} for _, test := range tests { - f := mustFieldVal64WithOverflow(test.in) - expected := mustFieldVal64(test.expected) - result := f.Inverse() + e := mustElementWithOverflow(test.in) + expected := mustElement(test.expected) + result := e.Inverse() if !result.Equals(expected) { t.Errorf("%s: wrong result\ngot: %x\nwant: %x", test.name, result.Bytes(), expected.Bytes()) @@ -1279,10 +1373,10 @@ func TestField64Inverse(t *testing.T) { } } -// TestField64IsGtOrEqPrimeMinusOrder ensures that field values report whether or +// TestElementIsGtOrEqPrimeMinusOrder ensures that elements report whether or // not they are greater than or equal to the field prime minus the group order // as expected for edge cases. -func TestField64IsGtOrEqPrimeMinusOrder(t *testing.T) { +func TestElementIsGtOrEqPrimeMinusOrder(t *testing.T) { tests := []struct { name string // test description in string // hex encoded test value @@ -1338,7 +1432,7 @@ func TestField64IsGtOrEqPrimeMinusOrder(t *testing.T) { }} for _, test := range tests { - result := mustFieldVal64(test.in).IsGtOrEqPrimeMinusOrder() + result := mustElement(test.in).IsGtOrEqPrimeMinusOrder() if result != test.expected { t.Errorf("%s: unexpected result -- got: %v, want: %v", test.name, result, test.expected) @@ -1347,11 +1441,11 @@ func TestField64IsGtOrEqPrimeMinusOrder(t *testing.T) { } } -// TestField64IsGtOrEqPrimeMinusOrderRandom ensures that field values report -// whether or not they are greater than or equal to the field prime minus the -// group order as expected by also performing the same operation with big ints -// and comparing the results. -func TestField64IsGtOrEqPrimeMinusOrderRandom(t *testing.T) { +// TestElementIsGtOrEqPrimeMinusOrderRandom ensures that elements report whether +// or not they are greater than or equal to the field prime minus the group +// order as expected by also performing the same operation with big ints and +// comparing the results. +func TestElementIsGtOrEqPrimeMinusOrderRandom(t *testing.T) { // Use a unique random seed each test instance and log it if the tests fail. seed := time.Now().Unix() rng := rand.New(rand.NewSource(seed)) @@ -1363,22 +1457,22 @@ func TestField64IsGtOrEqPrimeMinusOrderRandom(t *testing.T) { bigPMinusN := new(big.Int).Sub(curveParams.P, curveParams.N) for i := 0; i < 100; i++ { - // Generate big integer and field value with the same random value. - bigIntVal, fVal := randIntAndFieldVal64(t, rng) + // Generate big integer and element with the same random value. + bigIntVal, elem := randIntAndElement(t, rng) // Determine the value is greater than or equal to the prime minus the // order using big ints. bigIntResult := bigIntVal.Cmp(bigPMinusN) >= 0 // Determine the value is greater than or equal to the prime minus the - // order using a field value. - fValResult := fVal.IsGtOrEqPrimeMinusOrder() + // order using an element. + fValResult := elem.IsGtOrEqPrimeMinusOrder() // Ensure they match. if bigIntResult != fValResult { t.Fatalf("mismatched is gt or eq prime minus order\nbig int in: "+ - "%x\nscalar in: %x\nbig int result: %v\nscalar result %v", - bigIntVal, fVal.Bytes(), bigIntResult, fValResult) + "%x\nelem in: %x\nbig int result: %v\nelem result %v", + bigIntVal, elem.Bytes(), bigIntResult, fValResult) } } } diff --git a/dcrec/secp256k1/field4x64/elementisa_amd64.go b/dcrec/secp256k1/field4x64/elementisa_amd64.go new file mode 100644 index 0000000000..7a1215f64a --- /dev/null +++ b/dcrec/secp256k1/field4x64/elementisa_amd64.go @@ -0,0 +1,27 @@ +// Copyright (c) 2026 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +//go:build !purego + +package field4x64 + +// mulReduce sets r = a * b (mod p) using processor-specific hardware extensions +// when available. +func mulReduce(r *[4]uint64, a, b *[4]uint64) { + if useBMI2AndADX { + mulReduceADX(r, a, b) + return + } + mulReduceGeneric(r, a, b) +} + +// squareReduce sets r = a^2 (mod p) using processor-specific hardware +// extensions when available. +func squareReduce(r *[4]uint64, a *[4]uint64) { + if useBMI2AndADX { + squareReduceADX(r, a) + return + } + squareReduceGeneric(r, a) +} diff --git a/dcrec/secp256k1/field4x64/field64_bench_test.go b/dcrec/secp256k1/field4x64/field64_bench_test.go deleted file mode 100644 index e285b938a4..0000000000 --- a/dcrec/secp256k1/field4x64/field64_bench_test.go +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright (c) 2026 The Decred developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package field4x64 - -import ( - "testing" -) - -// BenchmarkField64Negate benchmarks calculating the additive inverse of an -// unsigned 256-bit big-endian integer modulo the field prime with [FieldVal64]. -func BenchmarkField64Negate(b *testing.B) { - // The function is constant time so any value is fine. - valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal64(valHex) - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - var result FieldVal64 - _ = result.NegateVal(f, 0) - } -} - -// BenchmarkField64Add benchmarks adding two unsigned 256-bit big-endian -// integers modulo the field prime with [FieldVal64]. -func BenchmarkField64Add(b *testing.B) { - a := mustFieldVal64("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab") - c := mustFieldVal64("16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca") - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - var sum FieldVal64 - sum.Add2(a, c) - } -} - -// BenchmarkField64MulBy2 benchmarks multiplying an unsigned 256-bit big-endian -// integer by 2 with [FieldVal64.MulBy2]. -func BenchmarkField64MulBy2(b *testing.B) { - fHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal64(fHex) - - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - f.MulBy2() - } -} - -// BenchmarkField64MulBy3 benchmarks multiplying an unsigned 256-bit big-endian -// integer by 3 with [FieldVal64.MulBy3]. -func BenchmarkField64MulBy3(b *testing.B) { - fHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal64(fHex) - - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - f.MulBy3() - } -} - -// BenchmarkField64MulBy4 benchmarks multiplying an unsigned 256-bit big-endian -// integer by 4 with [FieldVal64.MulBy4]. -func BenchmarkField64MulBy4(b *testing.B) { - fHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal64(fHex) - - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - f.MulBy4() - } -} - -// BenchmarkField64MulBy8 benchmarks multiplying an unsigned 256-bit big-endian -// integer by 8 with [FieldVal64.MulBy8]. -func BenchmarkField64MulBy8(b *testing.B) { - fHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal64(fHex) - - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - f.MulBy8() - } -} - -// BenchmarkFieldMulInt benchmarks multiplying an unsigned 256-bit big-endian -// integer by small integers with [FieldVal64.MulInt]. -func BenchmarkField64MulInt(b *testing.B) { - fHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal64(fHex) - - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - f.MulInt(2) - } -} - -// BenchmarkField64Mul benchmarks multiplying two unsigned 256-bit big-endian -// integers modulo the field prime with [FieldVal64]. -func BenchmarkField64Mul(b *testing.B) { - a := mustFieldVal64("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab") - c := mustFieldVal64("16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca") - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - var prod FieldVal64 - prod.Mul2(a, c) - } -} - -// BenchmarkField64Sqrt benchmarks calculating the square root of an unsigned -// 256-bit big-endian integer modulo the field prime with the FieldVal64 type. -func BenchmarkField64Sqrt(b *testing.B) { - // The function is constant time so any value is fine. - valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal64(valHex) - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - var result FieldVal64 - _ = result.SquareRootVal(f) - } -} - -// BenchmarkField64Square benchmarks squaring a 256-bit big-endian integer -// modulo the field prime with [FieldVal64]. -func BenchmarkField64Square(b *testing.B) { - a := mustFieldVal64("16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca") - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - var sq FieldVal64 - sq.SquareVal(a) - } -} - -// BenchmarkField64Inverse benchmarks calculating the multiplicative inverse of -// an unsigned 256-bit big-endian integer modulo the field prime with -// [FieldVal64]. -func BenchmarkField64Inverse(b *testing.B) { - // The function is constant time so any value is fine. - valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal64(valHex) - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - f.Inverse() - } -} - -// BenchmarkField64IsGtOrEqPrimeMinusOrder benchmarks determining whether a -// value is greater than or equal to the field prime minus the group order with -// [FieldVal64]. -func BenchmarkField64IsGtOrEqPrimeMinusOrder(b *testing.B) { - // The function is constant time so any value is fine. - valHex := "16fb970147a9acc73654d4be233cc48b875ce20a2122d24f073d29bd28805aca" - f := mustFieldVal64(valHex) - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = f.IsGtOrEqPrimeMinusOrder() - } -} diff --git a/dcrec/secp256k1/field4x64/field64_noasm.go b/dcrec/secp256k1/field4x64/field64_noasm.go deleted file mode 100644 index e2cb511ef7..0000000000 --- a/dcrec/secp256k1/field4x64/field64_noasm.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2026 The Decred developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -//go:build !amd64 || purego - -package field4x64 - -// field64UseBMI2AndADX depends on hardware support and access to assembly -// instructions. -var field64UseBMI2AndADX = false - -// field64MulReduce sets r = a * b (mod p). This defers to the generic -// implementation that does not rely on the optimized assembly implementations. -func field64MulReduce(r *[4]uint64, a, b *[4]uint64) { - field64MulReduceGeneric(r, a, b) -} - -// field64SquareReduce sets r = a^2 (mod p). This defers to the generic -// implementation that does not rely on the optimized assembly implementations. -func field64SquareReduce(r *[4]uint64, a *[4]uint64) { - field64SquareReduceGeneric(r, a) -} diff --git a/dcrec/secp256k1/field4x64/field64isa_amd64.go b/dcrec/secp256k1/field4x64/field64isa_amd64.go deleted file mode 100644 index 0d3bc6c2f9..0000000000 --- a/dcrec/secp256k1/field4x64/field64isa_amd64.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2026 The Decred developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -//go:build !purego - -package field4x64 - -// field64MulReduce sets r = a * b (mod p) using processor-specific hardware -// extensions when available. -func field64MulReduce(r *[4]uint64, a, b *[4]uint64) { - if field64UseBMI2AndADX { - field64MulReduceADX(r, a, b) - return - } - field64MulReduceGeneric(r, a, b) -} - -// field64SquareReduce sets r = a^2 (mod p) using processor-specific hardware -// extensions when available. -func field64SquareReduce(r *[4]uint64, a *[4]uint64) { - if field64UseBMI2AndADX { - field64SquareReduceADX(r, a) - return - } - field64SquareReduceGeneric(r, a) -} diff --git a/dcrec/secp256k1/internal/proofs/field_4x64_reduce_prover.py b/dcrec/secp256k1/internal/proofs/field_4x64_reduce_prover.py index 7b2b50b5ef..504b170f05 100644 --- a/dcrec/secp256k1/internal/proofs/field_4x64_reduce_prover.py +++ b/dcrec/secp256k1/internal/proofs/field_4x64_reduce_prover.py @@ -16,11 +16,11 @@ # by shorter mathematical variables for clarity. # Define P. -field64Prime0 = BitVecVal(0xfffffffefffffc2f, 64) -field64Prime1 = BitVecVal(0xffffffffffffffff, 64) -field64Prime2 = BitVecVal(0xffffffffffffffff, 64) -field64Prime3 = BitVecVal(0xffffffffffffffff, 64) -field64Prime = Concat(field64Prime3, field64Prime2, field64Prime1, field64Prime0) +fieldPrime0 = BitVecVal(0xfffffffefffffc2f, 64) +fieldPrime1 = BitVecVal(0xffffffffffffffff, 64) +fieldPrime2 = BitVecVal(0xffffffffffffffff, 64) +fieldPrime3 = BitVecVal(0xffffffffffffffff, 64) +fieldPrime = Concat(fieldPrime3, fieldPrime2, fieldPrime1, fieldPrime0) # Define 2P. twicePrime0 = BitVecVal(0xfffffffdfffff85e, 64) @@ -31,7 +31,7 @@ twicePrime = Concat(twicePrime4, twicePrime3, twicePrime2, twicePrime1, twicePrime0) # Define c = 2**256 - P. -field64PrimeComplement = BitVecVal(2**32+977, 64) +fieldPrimeComplement = BitVecVal(2**32+977, 64) # -------------------------------------------------------------------------- # Prove the constant definitions satisfy the required identities before @@ -39,8 +39,8 @@ # -------------------------------------------------------------------------- # Prove twicePrime = 2P. -field64Prime320 = ZeroExt(64, field64Prime) -prove(twicePrime == field64Prime320 << 1, "twicePrime != 2P") +fieldPrime320 = ZeroExt(64, fieldPrime) +prove(twicePrime == fieldPrime320 << 1, "twicePrime != 2P") # -------------------------------------------------------------------------- # Lemma R-identity: P + c == 2**256 @@ -51,8 +51,8 @@ # => 0 + c ≡ 2**256 (mod P) # => c ≡ 2**256 (mod P) # -------------------------------------------------------------------------- -field64PrimeComplement320 = ZeroExt(256, field64PrimeComplement) -prove(field64Prime320+field64PrimeComplement320 == BitVecVal(1, 320)<<256, +fieldPrimeComplement320 = ZeroExt(256, fieldPrimeComplement) +prove(fieldPrime320+fieldPrimeComplement320 == BitVecVal(1, 320)<<256, "Lemma R-identity: P+c != 2**256") # --------------- @@ -62,19 +62,19 @@ discards = [] # first reduction: 512 bits -> 289 bits. -h, t0 = mul64(x[4], field64PrimeComplement) +h, t0 = mul64(x[4], fieldPrimeComplement) -hi, lo = mul64(x[5], field64PrimeComplement) +hi, lo = mul64(x[5], fieldPrimeComplement) t1, carry = add64(lo, h, ZERO) h, discarded = add64(hi, ZERO, carry) discards.append(discarded) -hi, lo = mul64(x[6], field64PrimeComplement) +hi, lo = mul64(x[6], fieldPrimeComplement) t2, carry = add64(lo, h, ZERO) h, discarded = add64(hi, ZERO, carry) discards.append(discarded) -hi, lo = mul64(x[7], field64PrimeComplement) +hi, lo = mul64(x[7], fieldPrimeComplement) t3, carry = add64(lo, h, ZERO) t4, discarded = add64(hi, ZERO, carry) discards.append(discarded) @@ -90,7 +90,7 @@ t4_saved = t4 # second reduction: 289 bits -> t < 2P -h, t4 = mul64(t4, field64PrimeComplement) +h, t4 = mul64(t4, fieldPrimeComplement) t0, carry = add64(t0, t4, ZERO) t1, carry = add64(t1, h, carry) @@ -100,10 +100,10 @@ t4 = carry # final reduction: t < 2P -> t < P -s0, borrow = sub64(t0, field64Prime0, ZERO) -s1, borrow = sub64(t1, field64Prime1, borrow) -s2, borrow = sub64(t2, field64Prime2, borrow) -s3, borrow = sub64(t3, field64Prime3, borrow) +s0, borrow = sub64(t0, fieldPrime0, ZERO) +s1, borrow = sub64(t1, fieldPrime1, borrow) +s2, borrow = sub64(t2, fieldPrime2, borrow) +s3, borrow = sub64(t3, fieldPrime3, borrow) _, borrow = sub64(t4, ZERO, borrow) r0 = If(borrow == ONE, t0, s0) r1 = If(borrow == ONE, t1, s1) @@ -121,7 +121,7 @@ # # Since the complement is 33 bits, this proves the value does not exceed # 256 + 33 = 289 bits after the first reduction. -prove(ULE(t4_saved, field64PrimeComplement), "top limb after 1st reduction > complement") +prove(ULE(t4_saved, fieldPrimeComplement), "top limb after 1st reduction > complement") # Value after second reduction is less than twice the prime (t < 2P). t = Concat(t4, t3, t2, t1, t0) @@ -129,7 +129,7 @@ # Fully reduced result is less than the prime (r < P). r = Concat(r3, r2, r1, r0) -prove(ULT(r, field64Prime), "fully reduced result >= P") +prove(ULT(r, fieldPrime), "fully reduced result >= P") # ----------------------------------------------------------------------------- # Prove functional congruence: r ≡ x (mod P). @@ -155,7 +155,7 @@ # which contains no multipliers at all and reduces to adder-network equivalence # that the solver dispatches quickly. # -# The Go function, field64Reduce512, computes r in three stages: +# The Go function, field4x64.reduce512, computes r in three stages: # # 1) First reduction: Fold the high 256 bits of x (x4..x7) into the low 256 bits # (x0..x3). This produces t_1 = x_lo + x_hi*c which fits in 289 bits (5 @@ -169,7 +169,7 @@ # P which is exactly the definition of x ≡ r (mod P). def prove_functional_congruence_lemmas(): """ - Prove r == x (mod P) for field64Reduce512. + Prove r == x (mod P) for field4x64.reduce512. x is the 512-bit input (8 limbs x0..x7) and r is the fully-reduced 256-bit output (4 limbs). @@ -325,14 +325,14 @@ def pack_limbs(limbs): # and the 4-limb result wouldn't represent it. This fact is proven above # for the real total, so this does not smuggle in a new assumption. # ------------------------------------------------------------------------- - p = cwidth(field64Prime) + p = cwidth(fieldPrime) twop = cwidth(twicePrime) z = BitVecs("z0 z1 z2 z3 z4", 64) s = [None]*4 - s[0], borrow = sub64(z[0], field64Prime0, ZERO) - s[1], borrow = sub64(z[1], field64Prime1, borrow) - s[2], borrow = sub64(z[2], field64Prime2, borrow) - s[3], borrow = sub64(z[3], field64Prime3, borrow) + s[0], borrow = sub64(z[0], fieldPrime0, ZERO) + s[1], borrow = sub64(z[1], fieldPrime1, borrow) + s[2], borrow = sub64(z[2], fieldPrime2, borrow) + s[3], borrow = sub64(z[3], fieldPrime3, borrow) _, borrow = sub64(z[4], ZERO, borrow) t = pack_limbs(z) r = If(borrow == ONE, t, pack_limbs(s)) From 9181a8df88c931a2fbcbde20e425e8a4dd905605 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Wed, 29 Jul 2026 01:09:53 -0500 Subject: [PATCH 33/33] secp256k1: Add field4x64 README.md. This is part of a series of commits that will ultimately allow architecture-specific backend selection. --- dcrec/secp256k1/field4x64/README.md | 66 +++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 dcrec/secp256k1/field4x64/README.md diff --git a/dcrec/secp256k1/field4x64/README.md b/dcrec/secp256k1/field4x64/README.md new file mode 100644 index 0000000000..917788a7a7 --- /dev/null +++ b/dcrec/secp256k1/field4x64/README.md @@ -0,0 +1,66 @@ +field4x64 +========= + +[![Build Status](https://github.com/decred/dcrd/workflows/Build%20and%20Test/badge.svg)](https://github.com/decred/dcrd/actions) +[![ISC License](https://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![Doc](https://img.shields.io/badge/doc-reference-blue.svg)](https://pkg.go.dev/github.com/decred/dcrd/dcrec/secp256k1/v4/field4x64) + +Package field4x64 provides highly optimized pure-Go arithmetic over the +secp256k1 finite field using a 4x64 representation. + +It is designed for correctness, performance, security, and high assurance +through specialized arithmetic, constant time engineering, formal verification +of critical arithmetic routines, differential testing, and multiple +complementary testing techniques. + +This package exposes the underlying implementation directly and is primarily +intended for specialized use cases. Most consumers should prefer the +`secp256k1` package, which provides the stable public `FieldVal` type and +automatically selects the appropriate backend implementation for the target +architecture. + +## Design + +The implementation represents field elements with four 64-bit limbs using a +canonical 256-bit representation in the range [0, p-1]. Arithmetic is +specialized for the secp256k1 prime and uses optimized pure-Go implementations +together with architecture-specific assembly where available. + +Unlike the 10x26 backend, this representation fully reduces every arithmetic +operation. Consequently, field elements are always maintained in canonical form +and callers are not required to manually track normalization or magnitude. + +The semantics simplify both the API and implementation reasoning while remaining +highly efficient on modern 64-bit processors. + +## Assurance + +This package emphasizes correctness and implementation assurance through +multiple complementary validation techniques. + +Key implementation characteristics include: + +- Highly optimized arithmetic specialized specifically for the secp256k1 field +- Constant time implementations suitable for secret-dependent operations +- Formal verification of critical arithmetic operations using the Z3 theorem + prover +- Differential testing against independent secp256k1 implementations +- Deterministic test vectors +- Property-based testing +- Randomized-input testing +- Coverage-guided fuzz testing +- Manual review of security-critical implementation details + +## Disabling Assembler Optimizations + +The `purego` build tag may be used to disable all assembly code. + +## Installation and Updating + +This package is part of the `github.com/decred/dcrd/dcrec/secp256k1/v4` module. +Use the standard go tooling for working with modules to incorporate it. + +## License + +Package field4x64 is licensed under the [copyfree](http://copyfree.org) ISC +License.