external_validation_test.go raw
1 //go:build !js
2
3 package p256k1
4
5 import (
6 "crypto/sha256"
7 "encoding/hex"
8 "testing"
9 )
10
11 // TestExternalValidationGeneratorMult validates k*G against libsecp256k1
12 // This is the primary test for verifying our scalar multiplication is correct.
13 func TestExternalValidationGeneratorMult(t *testing.T) {
14 lib, err := GetLibSecp256k1()
15 if err != nil {
16 t.Skipf("libsecp256k1 not available: %v", err)
17 }
18 if !lib.IsLoaded() {
19 t.Skip("libsecp256k1 not loaded")
20 }
21
22 testCases := []struct {
23 name string
24 scalar string
25 }{
26 {"one", "0000000000000000000000000000000000000000000000000000000000000001"},
27 {"two", "0000000000000000000000000000000000000000000000000000000000000002"},
28 {"three", "0000000000000000000000000000000000000000000000000000000000000003"},
29 {"seven", "0000000000000000000000000000000000000000000000000000000000000007"},
30 {"small", "0000000000000000000000000000000000000000000000000000000000000100"},
31 {"random1", "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35"},
32 {"random2", "deadbeefcafebabe1234567890abcdef0123456789abcdef0123456789abcdef"},
33 {"half_order", "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0"},
34 // Edge cases for GLV splitting
35 {"lambda_minus_one", "5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd71"},
36 {"near_lambda", "5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd73"},
37 }
38
39 for _, tc := range testCases {
40 t.Run(tc.name, func(t *testing.T) {
41 seckey, _ := hex.DecodeString(tc.scalar)
42
43 // Get expected result from libsecp256k1
44 expectedPubkey, err := lib.CreatePubkey(seckey)
45 if err != nil {
46 t.Fatalf("libsecp256k1.CreatePubkey failed: %v", err)
47 }
48
49 // Parse scalar
50 var s Scalar
51 s.setB32(seckey)
52
53 if s.isZero() {
54 t.Skip("zero scalar")
55 }
56
57 // Test EcmultConst (reference binary method)
58 t.Run("EcmultConst", func(t *testing.T) {
59 var result GroupElementJacobian
60 EcmultConst(&result, &Generator, &s)
61 var aff GroupElementAffine
62 aff.setGEJ(&result)
63 aff.x.normalize()
64
65 var gotX [32]byte
66 aff.x.getB32(gotX[:])
67
68 if hex.EncodeToString(gotX[:]) != hex.EncodeToString(expectedPubkey) {
69 t.Errorf("EcmultConst mismatch:\n got: %s\n want: %s",
70 hex.EncodeToString(gotX[:]), hex.EncodeToString(expectedPubkey))
71 }
72 })
73
74 // Test ecmultGenGLV (GLV-optimized generator multiplication)
75 t.Run("ecmultGenGLV", func(t *testing.T) {
76 var result GroupElementJacobian
77 ecmultGenGLV(&result, &s)
78 var aff GroupElementAffine
79 aff.setGEJ(&result)
80 aff.x.normalize()
81
82 var gotX [32]byte
83 aff.x.getB32(gotX[:])
84
85 if hex.EncodeToString(gotX[:]) != hex.EncodeToString(expectedPubkey) {
86 t.Errorf("ecmultGenGLV mismatch:\n got: %s\n want: %s",
87 hex.EncodeToString(gotX[:]), hex.EncodeToString(expectedPubkey))
88 }
89 })
90
91 // Test ecmultStraussWNAFGLV with Generator
92 t.Run("ecmultStraussWNAFGLV", func(t *testing.T) {
93 var result GroupElementJacobian
94 ecmultStraussWNAFGLV(&result, &Generator, &s)
95 var aff GroupElementAffine
96 aff.setGEJ(&result)
97 aff.x.normalize()
98
99 var gotX [32]byte
100 aff.x.getB32(gotX[:])
101
102 if hex.EncodeToString(gotX[:]) != hex.EncodeToString(expectedPubkey) {
103 t.Errorf("ecmultStraussWNAFGLV mismatch:\n got: %s\n want: %s",
104 hex.EncodeToString(gotX[:]), hex.EncodeToString(expectedPubkey))
105 }
106 })
107
108 // Test ecmultWindowedVar
109 t.Run("ecmultWindowedVar", func(t *testing.T) {
110 var result GroupElementJacobian
111 ecmultWindowedVar(&result, &Generator, &s)
112 var aff GroupElementAffine
113 aff.setGEJ(&result)
114 aff.x.normalize()
115
116 var gotX [32]byte
117 aff.x.getB32(gotX[:])
118
119 if hex.EncodeToString(gotX[:]) != hex.EncodeToString(expectedPubkey) {
120 t.Errorf("ecmultWindowedVar mismatch:\n got: %s\n want: %s",
121 hex.EncodeToString(gotX[:]), hex.EncodeToString(expectedPubkey))
122 }
123 })
124 })
125 }
126 }
127
128 // TestExternalValidationECDH validates arbitrary point multiplication via ECDH
129 // libsecp256k1's ECDH computes hash(k*P), so if our k*P is correct, the hash should match.
130 func TestExternalValidationECDH(t *testing.T) {
131 lib, err := GetLibSecp256k1()
132 if err != nil {
133 t.Skipf("libsecp256k1 not available: %v", err)
134 }
135 if !lib.IsLoaded() {
136 t.Skip("libsecp256k1 not loaded")
137 }
138
139 // Test with different secret keys and public key points
140 testCases := []struct {
141 name string
142 seckey string
143 pubkeyGen string // scalar to generate the pubkey (pubkey = pubkeyGen * G)
144 }{
145 {"simple", "0000000000000000000000000000000000000000000000000000000000000002",
146 "0000000000000000000000000000000000000000000000000000000000000003"},
147 {"random", "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35",
148 "deadbeefcafebabe1234567890abcdef0123456789abcdef0123456789abcdef"},
149 }
150
151 for _, tc := range testCases {
152 t.Run(tc.name, func(t *testing.T) {
153 seckey, _ := hex.DecodeString(tc.seckey)
154 pubkeyGenBytes, _ := hex.DecodeString(tc.pubkeyGen)
155
156 // Generate the public key point using libsecp256k1
157 // First get the compressed pubkey for pubkeyGen * G
158 var pubkeyGenScalar Scalar
159 pubkeyGenScalar.setB32(pubkeyGenBytes)
160
161 // Compute pubkey = pubkeyGen * G using our reference implementation
162 var pubkeyJac GroupElementJacobian
163 EcmultConst(&pubkeyJac, &Generator, &pubkeyGenScalar)
164 var pubkeyAff GroupElementAffine
165 pubkeyAff.setGEJ(&pubkeyJac)
166 pubkeyAff.x.normalize()
167 pubkeyAff.y.normalize()
168
169 // Serialize as compressed pubkey (33 bytes)
170 pubkey33 := make([]byte, 33)
171 var xBytes [32]byte
172 pubkeyAff.x.getB32(xBytes[:])
173 copy(pubkey33[1:], xBytes[:])
174
175 // Determine prefix based on Y coordinate parity
176 var yBytes [32]byte
177 pubkeyAff.y.getB32(yBytes[:])
178 if yBytes[31]&1 == 0 {
179 pubkey33[0] = 0x02 // even Y
180 } else {
181 pubkey33[0] = 0x03 // odd Y
182 }
183
184 // Get expected ECDH result from libsecp256k1
185 expectedECDH, err := lib.ECDH(seckey, pubkey33)
186 if err != nil {
187 t.Fatalf("libsecp256k1.ECDH failed: %v", err)
188 }
189
190 // Now compute ECDH using our implementation
191 // ECDH = sha256(seckey * pubkey)
192 var seckeyScalar Scalar
193 seckeyScalar.setB32(seckey)
194
195 // Helper to compute ECDH hash matching libsecp256k1's default
196 // libsecp256k1 hashes sha256(0x02/0x03 || x) - the compressed point
197 computeECDH := func(result *GroupElementJacobian) [32]byte {
198 var resultAff GroupElementAffine
199 resultAff.setGEJ(result)
200 resultAff.x.normalize()
201 resultAff.y.normalize()
202
203 // Serialize as compressed point (33 bytes)
204 var compressed [33]byte
205 var xBytes [32]byte
206 resultAff.x.getB32(xBytes[:])
207 copy(compressed[1:], xBytes[:])
208
209 // Determine prefix based on Y parity
210 var yBytes [32]byte
211 resultAff.y.getB32(yBytes[:])
212 if yBytes[31]&1 == 0 {
213 compressed[0] = 0x02 // even Y
214 } else {
215 compressed[0] = 0x03 // odd Y
216 }
217
218 return sha256.Sum256(compressed[:])
219 }
220
221 // Test using EcmultConst (reference)
222 t.Run("EcmultConst", func(t *testing.T) {
223 var result GroupElementJacobian
224 EcmultConst(&result, &pubkeyAff, &seckeyScalar)
225 gotECDH := computeECDH(&result)
226
227 if hex.EncodeToString(gotECDH[:]) != hex.EncodeToString(expectedECDH) {
228 t.Errorf("ECDH mismatch:\n got: %s\n want: %s",
229 hex.EncodeToString(gotECDH[:]), hex.EncodeToString(expectedECDH))
230 }
231 })
232
233 // Test using ecmultStraussWNAFGLV
234 t.Run("ecmultStraussWNAFGLV", func(t *testing.T) {
235 var result GroupElementJacobian
236 ecmultStraussWNAFGLV(&result, &pubkeyAff, &seckeyScalar)
237 gotECDH := computeECDH(&result)
238
239 if hex.EncodeToString(gotECDH[:]) != hex.EncodeToString(expectedECDH) {
240 t.Errorf("ECDH mismatch:\n got: %s\n want: %s",
241 hex.EncodeToString(gotECDH[:]), hex.EncodeToString(expectedECDH))
242 }
243 })
244
245 // Test using ecmultWindowedVar
246 t.Run("ecmultWindowedVar", func(t *testing.T) {
247 var result GroupElementJacobian
248 ecmultWindowedVar(&result, &pubkeyAff, &seckeyScalar)
249 gotECDH := computeECDH(&result)
250
251 if hex.EncodeToString(gotECDH[:]) != hex.EncodeToString(expectedECDH) {
252 t.Errorf("ECDH mismatch:\n got: %s\n want: %s",
253 hex.EncodeToString(gotECDH[:]), hex.EncodeToString(expectedECDH))
254 }
255 })
256 })
257 }
258 }
259
260 // TestExternalValidationRandomScalars tests with pseudo-random scalars
261 func TestExternalValidationRandomScalars(t *testing.T) {
262 lib, err := GetLibSecp256k1()
263 if err != nil {
264 t.Skipf("libsecp256k1 not available: %v", err)
265 }
266 if !lib.IsLoaded() {
267 t.Skip("libsecp256k1 not loaded")
268 }
269
270 seed := uint64(0xdeadbeef12345678)
271
272 for i := 0; i < 100; i++ {
273 // Generate pseudo-random scalar
274 var seckey [32]byte
275 for j := 0; j < 32; j++ {
276 seed = seed*6364136223846793005 + 1442695040888963407
277 seckey[j] = byte(seed >> 56)
278 }
279
280 // Get expected from libsecp256k1
281 expectedPubkey, err := lib.CreatePubkey(seckey[:])
282 if err != nil {
283 continue // Skip invalid keys
284 }
285
286 var s Scalar
287 s.setB32(seckey[:])
288
289 if s.isZero() {
290 continue
291 }
292
293 // Test EcmultConst
294 var refResult GroupElementJacobian
295 EcmultConst(&refResult, &Generator, &s)
296 var refAff GroupElementAffine
297 refAff.setGEJ(&refResult)
298 refAff.x.normalize()
299
300 var refX [32]byte
301 refAff.x.getB32(refX[:])
302
303 if hex.EncodeToString(refX[:]) != hex.EncodeToString(expectedPubkey) {
304 t.Errorf("Random test %d: EcmultConst mismatch:\n scalar: %s\n got: %s\n want: %s",
305 i, hex.EncodeToString(seckey[:]),
306 hex.EncodeToString(refX[:]), hex.EncodeToString(expectedPubkey))
307 }
308
309 // Test ecmultStraussWNAFGLV
310 var straussResult GroupElementJacobian
311 ecmultStraussWNAFGLV(&straussResult, &Generator, &s)
312 var straussAff GroupElementAffine
313 straussAff.setGEJ(&straussResult)
314 straussAff.x.normalize()
315
316 var straussX [32]byte
317 straussAff.x.getB32(straussX[:])
318
319 if hex.EncodeToString(straussX[:]) != hex.EncodeToString(expectedPubkey) {
320 t.Errorf("Random test %d: ecmultStraussWNAFGLV mismatch:\n scalar: %s\n got: %s\n want: %s",
321 i, hex.EncodeToString(seckey[:]),
322 hex.EncodeToString(straussX[:]), hex.EncodeToString(expectedPubkey))
323 }
324
325 // Test ecmultGenGLV
326 var glvResult GroupElementJacobian
327 ecmultGenGLV(&glvResult, &s)
328 var glvAff GroupElementAffine
329 glvAff.setGEJ(&glvResult)
330 glvAff.x.normalize()
331
332 var glvX [32]byte
333 glvAff.x.getB32(glvX[:])
334
335 if hex.EncodeToString(glvX[:]) != hex.EncodeToString(expectedPubkey) {
336 t.Errorf("Random test %d: ecmultGenGLV mismatch:\n scalar: %s\n got: %s\n want: %s",
337 i, hex.EncodeToString(seckey[:]),
338 hex.EncodeToString(glvX[:]), hex.EncodeToString(expectedPubkey))
339 }
340 }
341 }
342
343 // TestExternalValidationGLVSplit validates the GLV scalar splitting specifically
344 // We can verify the split is correct by checking that k1 + k2*λ ≡ k (mod n)
345 // AND that k1*G + k2*(λ*G) = k*G (via libsecp256k1)
346 func TestExternalValidationGLVSplit(t *testing.T) {
347 lib, err := GetLibSecp256k1()
348 if err != nil {
349 t.Skipf("libsecp256k1 not available: %v", err)
350 }
351 if !lib.IsLoaded() {
352 t.Skip("libsecp256k1 not loaded")
353 }
354
355 testCases := []struct {
356 name string
357 scalar string
358 }{
359 {"one", "0000000000000000000000000000000000000000000000000000000000000001"},
360 {"random1", "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35"},
361 {"near_order", "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e"},
362 }
363
364 for _, tc := range testCases {
365 t.Run(tc.name, func(t *testing.T) {
366 kBytes, _ := hex.DecodeString(tc.scalar)
367
368 var k Scalar
369 k.setB32(kBytes)
370
371 if k.isZero() {
372 t.Skip("zero scalar")
373 }
374
375 // Get expected k*G from libsecp256k1
376 expectedPubkey, err := lib.CreatePubkey(kBytes)
377 if err != nil {
378 t.Fatalf("libsecp256k1.CreatePubkey failed: %v", err)
379 }
380
381 // Split k into k1, k2
382 var k1, k2 Scalar
383 scalarSplitLambda(&k1, &k2, &k)
384
385 // Log split values for debugging
386 var k1Bytes, k2Bytes [32]byte
387 k1.getB32(k1Bytes[:])
388 k2.getB32(k2Bytes[:])
389 t.Logf("k = %s", hex.EncodeToString(kBytes))
390 t.Logf("k1 = %s", hex.EncodeToString(k1Bytes[:]))
391 t.Logf("k2 = %s", hex.EncodeToString(k2Bytes[:]))
392
393 // Verify algebraically: k1 + k2*λ ≡ k (mod n)
394 var k2Lambda Scalar
395 k2Lambda.mul(&k2, &scalarLambda)
396
397 var reconstructed Scalar
398 reconstructed.add(&k1, &k2Lambda)
399
400 if !reconstructed.equal(&k) {
401 var recBytes [32]byte
402 reconstructed.getB32(recBytes[:])
403 t.Errorf("Scalar split reconstruction failed:\n k1 + k2*λ = %s\n k = %s",
404 hex.EncodeToString(recBytes[:]), hex.EncodeToString(kBytes))
405 }
406
407 // Verify geometrically: k1*G + k2*(λ*G) = k*G
408 // Compute k1*G
409 var k1G GroupElementJacobian
410 EcmultConst(&k1G, &Generator, &k1)
411
412 // Compute λ*G (using the endomorphism: (β*Gx, Gy))
413 var lambdaG GroupElementAffine
414 lambdaG.mulLambda(&Generator)
415
416 // Compute k2*(λ*G)
417 var k2LambdaG GroupElementJacobian
418 EcmultConst(&k2LambdaG, &lambdaG, &k2)
419
420 // Add them
421 var sum GroupElementJacobian
422 sum.addVar(&k1G, &k2LambdaG)
423
424 var sumAff GroupElementAffine
425 sumAff.setGEJ(&sum)
426 sumAff.x.normalize()
427
428 var sumX [32]byte
429 sumAff.x.getB32(sumX[:])
430
431 if hex.EncodeToString(sumX[:]) != hex.EncodeToString(expectedPubkey) {
432 t.Errorf("Geometric verification failed:\n k1*G + k2*(λ*G) = %s\n k*G (expected) = %s",
433 hex.EncodeToString(sumX[:]), hex.EncodeToString(expectedPubkey))
434 }
435 })
436 }
437 }
438
439 // TestExternalValidationSchnorr validates our Schnorr implementation against libsecp256k1
440 func TestExternalValidationSchnorr(t *testing.T) {
441 lib, err := GetLibSecp256k1()
442 if err != nil {
443 t.Skipf("libsecp256k1 not available: %v", err)
444 }
445 if !lib.IsLoaded() {
446 t.Skip("libsecp256k1 not loaded")
447 }
448
449 // Test that signatures created by libsecp256k1 can be verified by our code
450 // and vice versa
451 testCases := []struct {
452 name string
453 seckey string
454 msg string
455 }{
456 {"simple", "0000000000000000000000000000000000000000000000000000000000000001",
457 "0000000000000000000000000000000000000000000000000000000000000000"},
458 {"random", "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35",
459 "deadbeefcafebabe1234567890abcdef0123456789abcdef0123456789abcdef"},
460 }
461
462 for _, tc := range testCases {
463 t.Run(tc.name, func(t *testing.T) {
464 seckey, _ := hex.DecodeString(tc.seckey)
465 msg, _ := hex.DecodeString(tc.msg)
466
467 // Get pubkey from libsecp256k1
468 pubkey, err := lib.CreatePubkey(seckey)
469 if err != nil {
470 t.Fatalf("CreatePubkey failed: %v", err)
471 }
472
473 // Sign with libsecp256k1
474 sig, err := lib.SchnorrSign(msg, seckey)
475 if err != nil {
476 t.Fatalf("SchnorrSign failed: %v", err)
477 }
478
479 // Verify with libsecp256k1 (sanity check)
480 if !lib.SchnorrVerify(sig, msg, pubkey) {
481 t.Error("libsecp256k1 failed to verify its own signature")
482 }
483
484 // Verify with our implementation
485 xonlyPubkey, err := XOnlyPubkeyParse(pubkey)
486 if err != nil {
487 t.Fatalf("XOnlyPubkeyParse failed: %v", err)
488 }
489 valid := SchnorrVerify(sig, msg, xonlyPubkey)
490 if !valid {
491 t.Error("Our SchnorrVerify failed to verify libsecp256k1 signature")
492 }
493 })
494 }
495 }
496
497 // TestExternalValidationDebugSingleScalar is a focused debug test for a single scalar
498 func TestExternalValidationDebugSingleScalar(t *testing.T) {
499 lib, err := GetLibSecp256k1()
500 if err != nil {
501 t.Skipf("libsecp256k1 not available: %v", err)
502 }
503 if !lib.IsLoaded() {
504 t.Skip("libsecp256k1 not loaded")
505 }
506
507 // Use scalar 2 for simple debugging
508 seckey, _ := hex.DecodeString("0000000000000000000000000000000000000000000000000000000000000002")
509
510 expectedPubkey, err := lib.CreatePubkey(seckey)
511 if err != nil {
512 t.Fatalf("CreatePubkey failed: %v", err)
513 }
514 t.Logf("Expected (2*G).x = %s", hex.EncodeToString(expectedPubkey))
515
516 var s Scalar
517 s.setB32(seckey)
518
519 // EcmultConst
520 var constResult GroupElementJacobian
521 EcmultConst(&constResult, &Generator, &s)
522 var constAff GroupElementAffine
523 constAff.setGEJ(&constResult)
524 constAff.x.normalize()
525 var constX [32]byte
526 constAff.x.getB32(constX[:])
527 t.Logf("EcmultConst: %s", hex.EncodeToString(constX[:]))
528
529 // ecmultWindowedVar
530 var windowedResult GroupElementJacobian
531 ecmultWindowedVar(&windowedResult, &Generator, &s)
532 var windowedAff GroupElementAffine
533 windowedAff.setGEJ(&windowedResult)
534 windowedAff.x.normalize()
535 var windowedX [32]byte
536 windowedAff.x.getB32(windowedX[:])
537 t.Logf("ecmultWindowedVar: %s", hex.EncodeToString(windowedX[:]))
538
539 // ecmultGenGLV
540 var glvResult GroupElementJacobian
541 ecmultGenGLV(&glvResult, &s)
542 var glvAff GroupElementAffine
543 glvAff.setGEJ(&glvResult)
544 glvAff.x.normalize()
545 var glvX [32]byte
546 glvAff.x.getB32(glvX[:])
547 t.Logf("ecmultGenGLV: %s", hex.EncodeToString(glvX[:]))
548
549 // ecmultStraussWNAFGLV
550 var straussResult GroupElementJacobian
551 ecmultStraussWNAFGLV(&straussResult, &Generator, &s)
552 var straussAff GroupElementAffine
553 straussAff.setGEJ(&straussResult)
554 straussAff.x.normalize()
555 var straussX [32]byte
556 straussAff.x.getB32(straussX[:])
557 t.Logf("ecmultStraussWNAFGLV: %s", hex.EncodeToString(straussX[:]))
558
559 // Check all match expected
560 if hex.EncodeToString(constX[:]) != hex.EncodeToString(expectedPubkey) {
561 t.Errorf("EcmultConst MISMATCH")
562 }
563 if hex.EncodeToString(windowedX[:]) != hex.EncodeToString(expectedPubkey) {
564 t.Errorf("ecmultWindowedVar MISMATCH")
565 }
566 if hex.EncodeToString(glvX[:]) != hex.EncodeToString(expectedPubkey) {
567 t.Errorf("ecmultGenGLV MISMATCH")
568 }
569 if hex.EncodeToString(straussX[:]) != hex.EncodeToString(expectedPubkey) {
570 t.Errorf("ecmultStraussWNAFGLV MISMATCH")
571 }
572 }
573