From 5778c288e1f61af9dee23967c62871c4c31b0feb Mon Sep 17 00:00:00 2001 From: Clawd Date: Thu, 19 Feb 2026 21:24:07 -0800 Subject: Add bech32 encoding (npub/nsec for Nostr) --- bech32_test.go | 193 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 bech32_test.go (limited to 'bech32_test.go') diff --git a/bech32_test.go b/bech32_test.go new file mode 100644 index 0000000..276d998 --- /dev/null +++ b/bech32_test.go @@ -0,0 +1,193 @@ +package secp256k1 + +import ( + "strings" + "testing" +) + +func TestBech32EncodeBasic(t *testing.T) { + data := []byte{0x00, 0x01, 0x02} + encoded, err := Bech32Encode("test", data) + if err != nil { + t.Fatalf("encoding failed: %v", err) + } + + // Should start with hrp + "1" + if !strings.HasPrefix(encoded, "test1") { + t.Errorf("encoded should start with 'test1', got %s", encoded) + } +} + +func TestBech32RoundTrip(t *testing.T) { + data := []byte{0xde, 0xad, 0xbe, 0xef} + encoded, err := Bech32Encode("test", data) + if err != nil { + t.Fatalf("encoding failed: %v", err) + } + + hrp, decoded, err := Bech32Decode(encoded) + if err != nil { + t.Fatalf("decoding failed: %v", err) + } + + if hrp != "test" { + t.Errorf("hrp mismatch: got %s, want test", hrp) + } + + if len(decoded) != len(data) { + t.Fatalf("length mismatch: got %d, want %d", len(decoded), len(data)) + } + + for i := range data { + if decoded[i] != data[i] { + t.Errorf("byte %d mismatch: got %x, want %x", i, decoded[i], data[i]) + } + } +} + +func TestBech32DecodeInvalidChecksum(t *testing.T) { + // Valid encoding, then corrupt it + data := []byte{0x01, 0x02, 0x03} + encoded, _ := Bech32Encode("test", data) + + // Corrupt last character + corrupted := encoded[:len(encoded)-1] + "q" + + _, _, err := Bech32Decode(corrupted) + if err == nil { + t.Error("should reject invalid checksum") + } +} + +func TestBech32DecodeInvalidCharacter(t *testing.T) { + _, _, err := Bech32Decode("test1invalid!") + if err == nil { + t.Error("should reject invalid character") + } +} + +func TestNsecEncode(t *testing.T) { + priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + nsec := priv.Nsec() + + if !strings.HasPrefix(nsec, "nsec1") { + t.Errorf("nsec should start with 'nsec1', got %s", nsec) + } +} + +func TestNpubEncode(t *testing.T) { + priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + pub := priv.PublicKey() + npub := pub.Npub() + + if !strings.HasPrefix(npub, "npub1") { + t.Errorf("npub should start with 'npub1', got %s", npub) + } +} + +func TestNsecRoundTrip(t *testing.T) { + priv1, _ := GeneratePrivateKey() + nsec := priv1.Nsec() + + priv2, err := PrivateKeyFromNsec(nsec) + if err != nil { + t.Fatalf("failed to parse nsec: %v", err) + } + + if priv1.D.Cmp(priv2.D) != 0 { + t.Error("private key should survive nsec round-trip") + } +} + +func TestNpubRoundTrip(t *testing.T) { + priv, _ := GeneratePrivateKey() + pub1 := priv.PublicKey() + npub := pub1.Npub() + + pub2, err := PublicKeyFromNpub(npub) + if err != nil { + t.Fatalf("failed to parse npub: %v", err) + } + + // X coordinates should match (y might differ in sign) + if pub1.Point.x.value.Cmp(pub2.Point.x.value) != 0 { + t.Error("public key x should survive npub round-trip") + } +} + +func TestPrivateKeyFromNsecInvalid(t *testing.T) { + // Wrong prefix + _, err := PrivateKeyFromNsec("npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xjaeh") + if err == nil { + t.Error("should reject npub as nsec") + } +} + +func TestPublicKeyFromNpubInvalid(t *testing.T) { + // Wrong prefix + _, err := PublicKeyFromNpub("nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq0dcpx3") + if err == nil { + t.Error("should reject nsec as npub") + } +} + +// Test with known private key +func TestKnownNostrKeyPair(t *testing.T) { + // Private key = 1, should give G as public key + priv, _ := NewPrivateKeyFromHex("0000000000000000000000000000000000000000000000000000000000000001") + + nsec := priv.Nsec() + // Verify it starts with nsec and round-trips + if nsec[:5] != "nsec1" { + t.Errorf("nsec should start with nsec1, got %s", nsec) + } + + // Parse it back + priv2, err := PrivateKeyFromNsec(nsec) + if err != nil { + t.Fatalf("failed to parse nsec: %v", err) + } + if priv.D.Cmp(priv2.D) != 0 { + t.Error("nsec round-trip failed") + } + + // Public key should be G + pub := priv.PublicKey() + npub := pub.Npub() + + if npub[:5] != "npub1" { + t.Errorf("npub should start with npub1, got %s", npub) + } + + // Parse it back and verify x matches G + pub2, err := PublicKeyFromNpub(npub) + if err != nil { + t.Fatalf("failed to parse npub: %v", err) + } + if pub2.Point.x.value.Cmp(Gx) != 0 { + t.Error("npub should decode to G.x") + } +} + +func TestSignAndVerifyWithNostrKeys(t *testing.T) { + // Create keys + priv, _ := GeneratePrivateKey() + nsec := priv.Nsec() + npub := priv.PublicKey().Npub() + + // Parse them back + priv2, _ := PrivateKeyFromNsec(nsec) + pub2, _ := PublicKeyFromNpub(npub) + + // Sign with parsed private key + message := []byte("hello nostr") + sig, err := Sign(priv2, message) + if err != nil { + t.Fatalf("signing failed: %v", err) + } + + // Verify with parsed public key + if !Verify(pub2, message, sig) { + t.Error("signature should verify with parsed keys") + } +} -- cgit v1.2.3