From 6f3902d09504838e4e486825f073df971c412595 Mon Sep 17 00:00:00 2001 From: Clawd Date: Thu, 19 Feb 2026 21:20:09 -0800 Subject: Add Schnorr signatures (BIP-340 compatible) --- schnorr_test.go | 242 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 schnorr_test.go (limited to 'schnorr_test.go') diff --git a/schnorr_test.go b/schnorr_test.go new file mode 100644 index 0000000..acd709c --- /dev/null +++ b/schnorr_test.go @@ -0,0 +1,242 @@ +package secp256k1 + +import ( + "bytes" + "encoding/hex" + "math/big" + "testing" +) + +func TestTaggedHash(t *testing.T) { + // Just verify it produces 32 bytes + result := TaggedHash("test", []byte("hello")) + if len(result) != 32 { + t.Errorf("expected 32 bytes, got %d", len(result)) + } + + // Same inputs should produce same output + result2 := TaggedHash("test", []byte("hello")) + if !bytes.Equal(result, result2) { + t.Error("tagged hash should be deterministic") + } + + // Different tag should produce different output + result3 := TaggedHash("other", []byte("hello")) + if bytes.Equal(result, result3) { + t.Error("different tags should produce different hashes") + } +} + +func TestLiftX(t *testing.T) { + // Lift G.x should give us G (or its negation with even y) + p, err := liftX(Gx) + if err != nil { + t.Fatalf("failed to lift G.x: %v", err) + } + + if !p.IsOnCurve() { + t.Error("lifted point should be on curve") + } + + // x should match + if p.x.value.Cmp(Gx) != 0 { + t.Error("lifted x should match input") + } + + // y should be even (BIP-340 convention) + if !hasEvenY(p) { + t.Error("lifted point should have even y") + } +} + +func TestLiftXInvalid(t *testing.T) { + // x = 0 is not on the curve (0³ + 7 = 7, and 7 has no sqrt mod p) + _, err := liftX(big.NewInt(0)) + if err == nil { + t.Error("x=0 should not be on curve") + } +} + +func TestSignAndVerify(t *testing.T) { + priv, _ := GeneratePrivateKey() + pub := priv.PublicKey() + message := []byte("hello world") + + sig, err := Sign(priv, message) + if err != nil { + t.Fatalf("signing failed: %v", err) + } + + if !Verify(pub, message, sig) { + t.Error("signature should verify") + } +} + +func TestSignatureIsDeteministic(t *testing.T) { + priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + message := []byte("test message") + + sig1, _ := Sign(priv, message) + sig2, _ := Sign(priv, message) + + if sig1.R.Cmp(sig2.R) != 0 || sig1.S.Cmp(sig2.S) != 0 { + t.Error("BIP-340 signing should be deterministic") + } +} + +func TestVerifyWrongMessage(t *testing.T) { + priv, _ := GeneratePrivateKey() + pub := priv.PublicKey() + + sig, _ := Sign(priv, []byte("correct message")) + + if Verify(pub, []byte("wrong message"), sig) { + t.Error("signature should not verify with wrong message") + } +} + +func TestVerifyWrongPublicKey(t *testing.T) { + priv1, _ := GeneratePrivateKey() + priv2, _ := GeneratePrivateKey() + pub2 := priv2.PublicKey() + message := []byte("test") + + sig, _ := Sign(priv1, message) + + if Verify(pub2, message, sig) { + t.Error("signature should not verify with wrong public key") + } +} + +func TestVerifyTamperedSignature(t *testing.T) { + priv, _ := GeneratePrivateKey() + pub := priv.PublicKey() + message := []byte("test") + + sig, _ := Sign(priv, message) + + // Tamper with s + tamperedSig := &Signature{ + R: sig.R, + S: new(big.Int).Add(sig.S, big.NewInt(1)), + } + + if Verify(pub, message, tamperedSig) { + t.Error("tampered signature should not verify") + } +} + +func TestSignatureBytes(t *testing.T) { + priv, _ := GeneratePrivateKey() + message := []byte("test") + + sig, _ := Sign(priv, message) + b := sig.Bytes() + + if len(b) != 64 { + t.Errorf("signature should be 64 bytes, got %d", len(b)) + } +} + +func TestSignatureRoundTrip(t *testing.T) { + priv, _ := GeneratePrivateKey() + message := []byte("test") + + sig1, _ := Sign(priv, message) + b := sig1.Bytes() + sig2, err := SignatureFromBytes(b) + if err != nil { + t.Fatalf("failed to parse signature: %v", err) + } + + if sig1.R.Cmp(sig2.R) != 0 || sig1.S.Cmp(sig2.S) != 0 { + t.Error("signature should survive round-trip") + } +} + +func TestSignatureFromBytesInvalid(t *testing.T) { + _, err := SignatureFromBytes(make([]byte, 63)) + if err == nil { + t.Error("should reject wrong-length input") + } +} + +// BIP-340 Test Vector 0 +func TestBIP340Vector0(t *testing.T) { + privHex := "0000000000000000000000000000000000000000000000000000000000000003" + msgHex := "0000000000000000000000000000000000000000000000000000000000000000" + expectedSigHex := "e907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca821525f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0" + + priv, err := NewPrivateKeyFromHex(privHex) + if err != nil { + t.Fatalf("failed to parse private key: %v", err) + } + + msg, _ := hex.DecodeString(msgHex) + + sig, err := Sign(priv, msg) + if err != nil { + t.Fatalf("signing failed: %v", err) + } + + sigHex := sig.Hex() + if sigHex != expectedSigHex { + t.Errorf("signature mismatch\ngot: %s\nwant: %s", sigHex, expectedSigHex) + } + + // Also verify it + pub := priv.PublicKey() + if !Verify(pub, msg, sig) { + t.Error("BIP-340 test vector should verify") + } +} + +// BIP-340 Test Vector 1 +func TestBIP340Vector1(t *testing.T) { + privHex := "b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef" + msgHex := "243f6a8885a308d313198a2e03707344a4093822299f31d0082efa98ec4e6c89" + auxHex := "0000000000000000000000000000000000000000000000000000000000000001" + expectedSigHex := "6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de33418906d11ac976abccb20b091292bff4ea897efcb639ea871cfa95f6de339e4b0a" + + priv, err := NewPrivateKeyFromHex(privHex) + if err != nil { + t.Fatalf("failed to parse private key: %v", err) + } + + msg, _ := hex.DecodeString(msgHex) + aux, _ := hex.DecodeString(auxHex) + + sig, err := Sign(priv, msg, aux) + if err != nil { + t.Fatalf("signing failed: %v", err) + } + + sigHex := sig.Hex() + if sigHex != expectedSigHex { + t.Errorf("signature mismatch\ngot: %s\nwant: %s", sigHex, expectedSigHex) + } + + pub := priv.PublicKey() + if !Verify(pub, msg, sig) { + t.Error("BIP-340 test vector should verify") + } +} + +func TestXOnlyBytes(t *testing.T) { + priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + pub := priv.PublicKey() + + xOnly := pub.XOnlyBytes() + if len(xOnly) != 32 { + t.Errorf("x-only pubkey should be 32 bytes, got %d", len(xOnly)) + } + + // Should match G.x + expectedX := make([]byte, 32) + gxBytes := Gx.Bytes() + copy(expectedX[32-len(gxBytes):], gxBytes) + + if !bytes.Equal(xOnly, expectedX) { + t.Error("x-only bytes should match G.x") + } +} -- cgit v1.2.3