package address_test import ( "crypto/ed25519" "encoding/hex" "encoding/json" "math/rand" "strings" "testing" "github.com/btcsuite/btcd/btcutil/bech32" "git.n1ko.dev/Niko/niko_trust/internal/address" ) // deterministicKey returns a reproducible valid Ed25519 public key. func deterministicKey(t *testing.T, seedByte byte) ed25519.PublicKey { t.Helper() seed := make([]byte, ed25519.SeedSize) for i := range seed { seed[i] = seedByte } return ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey) } func TestRoundTrip(t *testing.T) { for i := 0; i < 256; i++ { pub := deterministicKey(t, byte(i)) a, err := address.FromPubKey(pub) if err != nil { t.Fatalf("FromPubKey: %v", err) } back, err := address.Parse(a.String()) if err != nil { t.Fatalf("Parse(%q): %v", a.String(), err) } if !back.Equal(a) { t.Fatalf("round trip changed address") } if !ed25519.PublicKey(back.PubKey()).Equal(pub) { t.Fatalf("public key not recovered exactly") } } } func TestAddressShape(t *testing.T) { pub := deterministicKey(t, 7) a, err := address.FromPubKey(pub) if err != nil { t.Fatal(err) } s := a.String() if !strings.HasPrefix(s, "trust1q") { t.Errorf("address must start with trust1q, got %q", s) } if len(s) != address.EncodedLen { t.Errorf("length = %d, want %d (%q)", len(s), address.EncodedLen, s) } if strings.ToLower(s) != s { t.Errorf("address must be lowercase") } if a.Version() != address.Version0 { t.Errorf("version = %d, want 0", a.Version()) } t.Logf("sample address: %s", s) } // TestKnownVector freezes the encoding. If this test fails, the wire format // changed and every previously issued address became invalid. func TestKnownVector(t *testing.T) { seed := make([]byte, 32) // all zero seed pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey) const wantPub = "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29" if got := hex.EncodeToString(pub); got != wantPub { t.Fatalf("test vector public key drifted: %s", got) } a, err := address.FromPubKey(pub) if err != nil { t.Fatal(err) } // Verified against an independent BIP-350 reference implementation, not // against this package's own output. const want = "trust1qqak5faue6m2gttz5w5dq2n0p4ek2vs4wuw7ysax8tqy3gvtt8dzj0yfahr" if a.String() != want { t.Fatalf("encoding drifted:\n got %s\nwant %s", a.String(), want) } } func TestParseRejectsInvalid(t *testing.T) { valid, err := address.FromPubKey(deterministicKey(t, 3)) if err != nil { t.Fatal(err) } v := valid.String() tests := []struct { name string in string want error }{ {"empty", "", address.ErrEmpty}, {"uppercase", strings.ToUpper(v), address.ErrNotLowercase}, {"mixed case", strings.ToUpper(v[:6]) + v[6:], address.ErrNotLowercase}, {"too long", v + "qqqqqqqqqq", address.ErrTooLong}, {"no separator", strings.ReplaceAll(v, "1", "q"), address.ErrChecksum}, {"truncated", v[:len(v)-1], address.ErrChecksum}, {"extended", v + "q", address.ErrTooLong}, {"bare hrp", "trust1", address.ErrChecksum}, {"garbage", "not-an-address", address.ErrChecksum}, {"invalid char b", strings.Replace(v, "q", "b", 1), address.ErrChecksum}, {"space", "trust1 " + v[7:], address.ErrChecksum}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { _, err := address.Parse(tc.in) if err == nil { t.Fatalf("accepted invalid input %q", tc.in) } if tc.want != nil && !errorIs(err, tc.want) { t.Fatalf("error = %v, want %v", err, tc.want) } }) } } func errorIs(err, target error) bool { return err != nil && (err == target || strings.Contains(err.Error(), target.Error())) } // TestRejectsBech32NotBech32m is the checksum downgrade test. The same payload // encoded with the original bech32 constant must be refused, otherwise two // different strings would denote one identity and a caller could be tricked // into accepting a foreign address family. func TestRejectsBech32NotBech32m(t *testing.T) { pub := deterministicKey(t, 11) payload := append([]byte{address.Version0}, pub...) conv, err := bech32.ConvertBits(payload, 8, 5, true) if err != nil { t.Fatal(err) } legacy, err := bech32.Encode(address.HRP, conv) // bech32, not bech32m if err != nil { t.Fatal(err) } modern, err := bech32.EncodeM(address.HRP, conv) if err != nil { t.Fatal(err) } if legacy == modern { t.Fatal("test is meaningless: both constants produced the same string") } if _, err := address.Parse(legacy); !errorIs(err, address.ErrNotBech32m) { t.Fatalf("bech32 (non-m) accepted or wrong error: %v", err) } if _, err := address.Parse(modern); err != nil { t.Fatalf("bech32m rejected: %v", err) } } func TestRejectsWrongHRP(t *testing.T) { pub := deterministicKey(t, 13) payload := append([]byte{address.Version0}, pub...) conv, _ := bech32.ConvertBits(payload, 8, 5, true) for _, hrp := range []string{"bc", "trus", "trustx", "tb", "trust2"} { s, err := bech32.EncodeM(hrp, conv) if err != nil { t.Fatalf("encode %q: %v", hrp, err) } if _, err := address.Parse(s); err == nil { t.Fatalf("accepted foreign hrp %q", hrp) } } } func TestRejectsUnknownVersion(t *testing.T) { pub := deterministicKey(t, 17) for _, ver := range []byte{1, 2, 0x7f, 0xff} { payload := append([]byte{ver}, pub...) conv, _ := bech32.ConvertBits(payload, 8, 5, true) s, _ := bech32.EncodeM(address.HRP, conv) if _, err := address.Parse(s); !errorIs(err, address.ErrVersion) { t.Fatalf("version %d: err = %v, want ErrVersion", ver, err) } } } func TestRejectsWrongPayloadSize(t *testing.T) { for _, n := range []int{0, 1, 16, 31, 33, 40} { payload := make([]byte, n+1) conv, _ := bech32.ConvertBits(payload, 8, 5, true) s, err := bech32.EncodeM(address.HRP, conv) if err != nil { continue } if _, err := address.Parse(s); err == nil { t.Fatalf("accepted payload of %d key bytes", n) } } } // TestChecksumDetectsSingleCharCorruption is the core integrity property of // bech32: any single character substitution must invalidate the string. A // corrupted address must never silently decode to a different valid identity, // because that would send a claim or an approval to the wrong party. func TestChecksumDetectsSingleCharCorruption(t *testing.T) { const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" valid, err := address.FromPubKey(deterministicKey(t, 23)) if err != nil { t.Fatal(err) } v := valid.String() sep := strings.LastIndexByte(v, '1') mutations, accepted := 0, 0 for i := sep + 1; i < len(v); i++ { for _, c := range charset { if byte(c) == v[i] { continue } mutated := v[:i] + string(c) + v[i+1:] mutations++ got, err := address.Parse(mutated) if err == nil { accepted++ if got.Equal(valid) { t.Fatalf("corruption produced the same identity") } t.Errorf("single-char corruption at %d accepted: %s", i, mutated) } } } if mutations == 0 { t.Fatal("no mutations generated") } t.Logf("tested %d single-character corruptions, %d accepted", mutations, accepted) } // TestTwoCharSwapDetected covers transposition, the most common human // transcription error. func TestTwoCharSwapDetected(t *testing.T) { valid, _ := address.FromPubKey(deterministicKey(t, 29)) v := valid.String() sep := strings.LastIndexByte(v, '1') swaps, accepted := 0, 0 for i := sep + 1; i < len(v)-1; i++ { if v[i] == v[i+1] { continue } m := v[:i] + string(v[i+1]) + string(v[i]) + v[i+2:] swaps++ if _, err := address.Parse(m); err == nil { accepted++ t.Errorf("transposition at %d accepted: %s", i, m) } } t.Logf("tested %d transpositions, %d accepted", swaps, accepted) } func TestEqualAndZero(t *testing.T) { a, _ := address.FromPubKey(deterministicKey(t, 31)) b, _ := address.FromPubKey(deterministicKey(t, 31)) c, _ := address.FromPubKey(deterministicKey(t, 32)) if !a.Equal(b) { t.Error("same key must be equal") } if a.Equal(c) { t.Error("different keys must not be equal") } var zero address.Address if !zero.IsZero() { t.Error("zero value must report IsZero") } if zero.Equal(zero) { t.Error("zero address must not compare equal to itself; it denotes no identity") } if a.Equal(zero) || zero.Equal(a) { t.Error("zero must not equal a real address") } if zero.String() != "" { t.Error("zero address must stringify to empty") } } // TestPubKeyIsCopied guards against a caller mutating the key inside a // validated Address, which would let validated and actual key diverge. func TestPubKeyIsCopied(t *testing.T) { a, _ := address.FromPubKey(deterministicKey(t, 37)) k1 := a.PubKey() for i := range k1 { k1[i] = 0xff } k2 := a.PubKey() for i := range k2 { if k2[i] == 0xff { t.Fatal("mutating a returned key changed the Address") } } if _, err := address.Parse(a.String()); err != nil { t.Fatalf("address became invalid after caller mutation: %v", err) } } func TestTextMarshaling(t *testing.T) { a, _ := address.FromPubKey(deterministicKey(t, 41)) b, err := json.Marshal(a) if err != nil { t.Fatal(err) } if string(b) != `"`+a.String()+`"` { t.Fatalf("json = %s", b) } var back address.Address if err := json.Unmarshal(b, &back); err != nil { t.Fatal(err) } if !back.Equal(a) { t.Fatal("json round trip failed") } // Unmarshaling must run full validation, not just store the string. var bad address.Address if err := json.Unmarshal([]byte(`"trust1qinvalid"`), &bad); err == nil { t.Fatal("unmarshal accepted an invalid address") } var zero address.Address if _, err := json.Marshal(zero); err == nil { t.Fatal("marshaling the zero address must fail") } } func TestMustParsePanics(t *testing.T) { defer func() { if recover() == nil { t.Fatal("MustParse did not panic on invalid input") } }() address.MustParse("trust1qnope") } // TestNoTwoStringsForOneKey asserts canonicality (INV-8) over random keys. func TestNoTwoStringsForOneKey(t *testing.T) { rng := rand.New(rand.NewSource(1)) seen := make(map[[32]byte]string) for i := 0; i < 500; i++ { seed := make([]byte, ed25519.SeedSize) rng.Read(seed) pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey) a, err := address.FromPubKey(pub) if err != nil { t.Fatal(err) } key := a.KeyBytes() if prev, ok := seen[key]; ok && prev != a.String() { t.Fatalf("one key produced two addresses: %s and %s", prev, a.String()) } seen[key] = a.String() // Encoding the same key twice must be byte-identical. again, _ := address.FromPubKey(pub) if again.String() != a.String() { t.Fatal("encoding is not deterministic") } } } func BenchmarkParse(b *testing.B) { seed := make([]byte, ed25519.SeedSize) pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey) a, _ := address.FromPubKey(pub) s := a.String() b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { if _, err := address.Parse(s); err != nil { b.Fatal(err) } } }