- Port field operations assembler from libsecp256k1 (field_amd64.s,
field_amd64_bmi2.s) with MULX/ADCX/ADOX instructions
- Add AVX2 scalar and affine point operations in avx/ package
- Implement CPU feature detection (cpufeatures.go) for AVX2/BMI2
- Add libsecp256k1.so via purego for native C library comparison
- Create comprehensive SIMD benchmark suite comparing btcec, P256K1
pure Go, P256K1 ASM, and libsecp256k1
- Add BENCHMARK_SIMD.md documenting performance across implementations
- Remove BtcecSigner, consolidate on P256K1Signer as primary impl
- Add field operation tests and benchmarks (field_asm_test.go,
field_bench_test.go)
- Update GLV endomorphism with wNAF scalar multiplication
- Add scalar assembly (scalar_amd64.s) for optimized operations
- Clean up dependencies and update benchmark reports
319 lines
7.3 KiB
Go
319 lines
7.3 KiB
Go
package p256k1
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
)
|
|
|
|
func TestGroupElementAffine(t *testing.T) {
|
|
// Test infinity point
|
|
var inf GroupElementAffine
|
|
inf.setInfinity()
|
|
if !inf.isInfinity() {
|
|
t.Error("setInfinity should create infinity point")
|
|
}
|
|
if !inf.isValid() {
|
|
t.Error("infinity point should be valid")
|
|
}
|
|
|
|
// Test generator point
|
|
if Generator.isInfinity() {
|
|
t.Error("generator should not be infinity")
|
|
}
|
|
if !Generator.isValid() {
|
|
t.Error("generator should be valid")
|
|
}
|
|
|
|
// Test point negation
|
|
var neg GroupElementAffine
|
|
neg.negate(&Generator)
|
|
if neg.isInfinity() {
|
|
t.Error("negated generator should not be infinity")
|
|
}
|
|
if !neg.isValid() {
|
|
t.Error("negated generator should be valid")
|
|
}
|
|
|
|
// Test that G + (-G) = O (using Jacobian arithmetic)
|
|
var gJac, negJac, result GroupElementJacobian
|
|
gJac.setGE(&Generator)
|
|
negJac.setGE(&neg)
|
|
result.addVar(&gJac, &negJac)
|
|
if !result.isInfinity() {
|
|
t.Error("G + (-G) should equal infinity")
|
|
}
|
|
}
|
|
|
|
func TestGroupElementJacobian(t *testing.T) {
|
|
// Test conversion between affine and Jacobian
|
|
var jac GroupElementJacobian
|
|
var aff GroupElementAffine
|
|
|
|
// Convert generator to Jacobian and back
|
|
jac.setGE(&Generator)
|
|
aff.setGEJ(&jac)
|
|
|
|
if !aff.equal(&Generator) {
|
|
t.Error("conversion G -> Jacobian -> affine should preserve point")
|
|
}
|
|
|
|
// Test point doubling
|
|
var doubled GroupElementJacobian
|
|
doubled.double(&jac)
|
|
if doubled.isInfinity() {
|
|
t.Error("2*G should not be infinity")
|
|
}
|
|
|
|
// Convert back to affine to validate
|
|
var doubledAff GroupElementAffine
|
|
doubledAff.setGEJ(&doubled)
|
|
if !doubledAff.isValid() {
|
|
t.Error("2*G should be valid point")
|
|
}
|
|
}
|
|
|
|
func TestGroupElementStorage(t *testing.T) {
|
|
// Test storage conversion
|
|
var storage GroupElementStorage
|
|
var restored GroupElementAffine
|
|
|
|
// Store and restore generator
|
|
Generator.toStorage(&storage)
|
|
restored.fromStorage(&storage)
|
|
|
|
if !restored.equal(&Generator) {
|
|
t.Error("storage conversion should preserve point")
|
|
}
|
|
|
|
// Test infinity storage
|
|
var inf GroupElementAffine
|
|
inf.setInfinity()
|
|
inf.toStorage(&storage)
|
|
restored.fromStorage(&storage)
|
|
|
|
if !restored.isInfinity() {
|
|
t.Error("infinity should be preserved in storage")
|
|
}
|
|
}
|
|
|
|
func TestGroupElementBytes(t *testing.T) {
|
|
var buf [64]byte
|
|
var restored GroupElementAffine
|
|
|
|
// Test generator conversion
|
|
Generator.toBytes(buf[:])
|
|
restored.fromBytes(buf[:])
|
|
|
|
if !restored.equal(&Generator) {
|
|
t.Error("byte conversion should preserve point")
|
|
}
|
|
|
|
// Test infinity conversion
|
|
var inf GroupElementAffine
|
|
inf.setInfinity()
|
|
inf.toBytes(buf[:])
|
|
restored.fromBytes(buf[:])
|
|
|
|
if !restored.isInfinity() {
|
|
t.Error("infinity should be preserved in byte conversion")
|
|
}
|
|
}
|
|
|
|
func BenchmarkGroupDouble(b *testing.B) {
|
|
var jac GroupElementJacobian
|
|
jac.setGE(&Generator)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
jac.double(&jac)
|
|
}
|
|
}
|
|
|
|
func BenchmarkGroupAdd(b *testing.B) {
|
|
var jac1, jac2 GroupElementJacobian
|
|
jac1.setGE(&Generator)
|
|
jac2.setGE(&Generator)
|
|
jac2.double(&jac2) // Make it 2*G
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
jac1.addVar(&jac1, &jac2)
|
|
}
|
|
}
|
|
|
|
// TestBatchNormalize tests that BatchNormalize produces the same results as individual conversions
|
|
func TestBatchNormalize(t *testing.T) {
|
|
// Create several Jacobian points: G, 2G, 3G, 4G, ...
|
|
n := 10
|
|
points := make([]GroupElementJacobian, n)
|
|
expected := make([]GroupElementAffine, n)
|
|
|
|
var current GroupElementJacobian
|
|
current.setGE(&Generator)
|
|
|
|
for i := 0; i < n; i++ {
|
|
points[i] = current
|
|
// Get expected result using individual conversion
|
|
expected[i].setGEJ(¤t)
|
|
// Move to next point
|
|
var next GroupElementJacobian
|
|
next.addVar(¤t, &points[0]) // Add G each time
|
|
current = next
|
|
}
|
|
|
|
// Now use BatchNormalize
|
|
result := BatchNormalize(nil, points)
|
|
|
|
// Compare results
|
|
for i := 0; i < n; i++ {
|
|
// Normalize both for comparison
|
|
expected[i].x.normalize()
|
|
expected[i].y.normalize()
|
|
result[i].x.normalize()
|
|
result[i].y.normalize()
|
|
|
|
if !expected[i].x.equal(&result[i].x) {
|
|
t.Errorf("Point %d: X mismatch", i)
|
|
}
|
|
if !expected[i].y.equal(&result[i].y) {
|
|
t.Errorf("Point %d: Y mismatch", i)
|
|
}
|
|
if expected[i].infinity != result[i].infinity {
|
|
t.Errorf("Point %d: infinity mismatch", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBatchNormalizeWithInfinity tests that BatchNormalize handles infinity points correctly
|
|
func TestBatchNormalizeWithInfinity(t *testing.T) {
|
|
points := make([]GroupElementJacobian, 5)
|
|
|
|
// Set some points to generator, some to infinity
|
|
points[0].setGE(&Generator)
|
|
points[1].setInfinity()
|
|
points[2].setGE(&Generator)
|
|
points[2].double(&points[2]) // 2G
|
|
points[3].setInfinity()
|
|
points[4].setGE(&Generator)
|
|
|
|
result := BatchNormalize(nil, points)
|
|
|
|
// Check infinity points
|
|
if !result[1].isInfinity() {
|
|
t.Error("Point 1 should be infinity")
|
|
}
|
|
if !result[3].isInfinity() {
|
|
t.Error("Point 3 should be infinity")
|
|
}
|
|
|
|
// Check non-infinity points
|
|
if result[0].isInfinity() {
|
|
t.Error("Point 0 should not be infinity")
|
|
}
|
|
if result[2].isInfinity() {
|
|
t.Error("Point 2 should not be infinity")
|
|
}
|
|
if result[4].isInfinity() {
|
|
t.Error("Point 4 should not be infinity")
|
|
}
|
|
|
|
// Verify non-infinity points are on the curve
|
|
if !result[0].isValid() {
|
|
t.Error("Point 0 should be valid")
|
|
}
|
|
if !result[2].isValid() {
|
|
t.Error("Point 2 should be valid")
|
|
}
|
|
if !result[4].isValid() {
|
|
t.Error("Point 4 should be valid")
|
|
}
|
|
}
|
|
|
|
// TestBatchNormalizeInPlace tests in-place batch normalization
|
|
func TestBatchNormalizeInPlace(t *testing.T) {
|
|
n := 5
|
|
points := make([]GroupElementJacobian, n)
|
|
expected := make([]GroupElementAffine, n)
|
|
|
|
var current GroupElementJacobian
|
|
current.setGE(&Generator)
|
|
|
|
for i := 0; i < n; i++ {
|
|
points[i] = current
|
|
expected[i].setGEJ(¤t)
|
|
var next GroupElementJacobian
|
|
next.addVar(¤t, &points[0])
|
|
current = next
|
|
}
|
|
|
|
// Normalize in place
|
|
BatchNormalizeInPlace(points)
|
|
|
|
// After normalization, Z should be 1 for all non-infinity points
|
|
for i := 0; i < n; i++ {
|
|
if !points[i].isInfinity() {
|
|
var one FieldElement
|
|
one.setInt(1)
|
|
points[i].z.normalize()
|
|
if !points[i].z.equal(&one) {
|
|
t.Errorf("Point %d: Z should be 1 after normalization", i)
|
|
}
|
|
}
|
|
|
|
// Check X and Y match expected
|
|
points[i].x.normalize()
|
|
points[i].y.normalize()
|
|
expected[i].x.normalize()
|
|
expected[i].y.normalize()
|
|
|
|
if !points[i].x.equal(&expected[i].x) {
|
|
t.Errorf("Point %d: X mismatch after in-place normalization", i)
|
|
}
|
|
if !points[i].y.equal(&expected[i].y) {
|
|
t.Errorf("Point %d: Y mismatch after in-place normalization", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkBatchNormalize benchmarks batch normalization vs individual conversions
|
|
func BenchmarkBatchNormalize(b *testing.B) {
|
|
sizes := []int{1, 2, 4, 8, 16, 32, 64}
|
|
|
|
for _, size := range sizes {
|
|
n := size // capture for closure
|
|
|
|
// Create n Jacobian points
|
|
points := make([]GroupElementJacobian, n)
|
|
var current GroupElementJacobian
|
|
current.setGE(&Generator)
|
|
for i := 0; i < n; i++ {
|
|
points[i] = current
|
|
current.double(¤t)
|
|
}
|
|
|
|
b.Run(
|
|
fmt.Sprintf("Individual_%d", n),
|
|
func(b *testing.B) {
|
|
out := make([]GroupElementAffine, n)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
for j := 0; j < n; j++ {
|
|
out[j].setGEJ(&points[j])
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
b.Run(
|
|
fmt.Sprintf("Batch_%d", n),
|
|
func(b *testing.B) {
|
|
out := make([]GroupElementAffine, n)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
BatchNormalize(out, points)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|