diff --git a/bech32/bech32.go b/bech32/bech32.go index c18fe74..7aa307d 100644 --- a/bech32/bech32.go +++ b/bech32/bech32.go @@ -150,12 +150,13 @@ func bech32VerifyChecksum(hrp string, data []byte) bool { } // DecodeNoLimit decodes a bech32 encoded string, returning the human-readable -// part and the data part excluding the checksum. This function does NOT +// part and the data part excluding the checksum. This function does NOT // validate against the BIP-173 maximum length allowed for bech32 strings and // is meant for use in custom applications (such as lightning network payment // requests), NOT on-chain addresses. // -// Note that the returned data is 5-bit (base32) encoded. +// Note that the returned data is 5-bit (base32) encoded and the human-readable +// part will be lowercase. func DecodeNoLimit(bech string) (string, []byte, error) { // The minimum allowed size of a bech32 string is 8 characters, since it // needs a non-empty HRP, a separator, and a 6 character checksum. @@ -234,7 +235,8 @@ func DecodeNoLimit(bech string) (string, []byte, error) { // Decode decodes a bech32 encoded string, returning the human-readable part and // the data part excluding the checksum. // -// Note that the returned data is 5-bit (base32) encoded. +// Note that the returned data is 5-bit (base32) encoded and the human-readable +// part will be lowercase. func Decode(bech string) (string, []byte, error) { // The maximum allowed length for a bech32 string is 90. if len(bech) > 90 { @@ -244,13 +246,14 @@ func Decode(bech string) (string, []byte, error) { return DecodeNoLimit(bech) } -// Encode encodes a byte slice into a bech32 string with the -// human-readable part hrb. Note that the bytes must each encode 5 bits -// (base32). +// Encode encodes a byte slice into a bech32 string with the given +// human-readable part (HRP). The HRP will be converted to lowercase if needed +// since mixed cased encodings are not permitted and lowercase is used for +// checksum purposes. Note that the bytes must each encode 5 bits (base32). func Encode(hrp string, data []byte) (string, error) { - - // The resulting bech32 string is the concatenation of the hrp, the - // separator 1, data and the 6-byte checksum. + // The resulting bech32 string is the concatenation of the lowercase hrp, + // the separator 1, data and the 6-byte checksum. + hrp = strings.ToLower(hrp) var bldr strings.Builder bldr.Grow(len(hrp) + 1 + len(data) + 6) bldr.WriteString(hrp) diff --git a/bech32/bech32_test.go b/bech32/bech32_test.go index af4392d..f6fe773 100644 --- a/bech32/bech32_test.go +++ b/bech32/bech32_test.go @@ -86,6 +86,95 @@ func TestBech32(t *testing.T) { } } +// TestMixedCaseEncode ensures mixed case HRPs are converted to lowercase as +// expected when encoding and that decoding the produced encoding when converted +// to all uppercase produces the lowercase HRP and original data. +func TestMixedCaseEncode(t *testing.T) { + tests := []struct { + name string + hrp string + data string + encoded string + }{{ + name: "all uppercase HRP with no data", + hrp: "A", + data: "", + encoded: "a12uel5l", + }, { + name: "all uppercase HRP with data", + hrp: "UPPERCASE", + data: "787878", + encoded: "uppercase10pu8sss7kmp", + }, { + name: "mixed case HRP even offsets uppercase", + hrp: "AbCdEf", + data: "00443214c74254b635cf84653a56d7c675be77df", + encoded: "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", + }, { + name: "mixed case HRP odd offsets uppercase ", + hrp: "aBcDeF", + data: "00443214c74254b635cf84653a56d7c675be77df", + encoded: "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", + }, { + name: "all lowercase HRP", + hrp: "abcdef", + data: "00443214c74254b635cf84653a56d7c675be77df", + encoded: "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", + }} + + for _, test := range tests { + // Convert the text hex to bytes, convert those bytes from base256 to + // base32, then ensure the encoded result with the HRP provided in the + // test data is as expected. + data, err := hex.DecodeString(test.data) + if err != nil { + t.Errorf("%q: invalid hex %q: %v", test.name, test.data, err) + continue + } + convertedData, err := ConvertBits(data, 8, 5, true) + if err != nil { + t.Errorf("%q: unexpected convert bits error: %v", test.name, + err) + continue + } + gotEncoded, err := Encode(test.hrp, convertedData) + if err != nil { + t.Errorf("%q: unexpected encode error: %v", test.name, err) + continue + } + if gotEncoded != test.encoded { + t.Errorf("%q: mismatched encoding -- got %q, want %q", test.name, + gotEncoded, test.encoded) + continue + } + + // Ensure the decoding the expected lowercase encoding converted to all + // uppercase produces the lowercase HRP and original data. + gotHRP, gotData, err := Decode(strings.ToUpper(test.encoded)) + if err != nil { + t.Errorf("%q: unexpected decode error: %v", test.name, err) + continue + } + wantHRP := strings.ToLower(test.hrp) + if gotHRP != wantHRP { + t.Errorf("%q: mismatched decoded HRP -- got %q, want %q", test.name, + gotHRP, wantHRP) + continue + } + convertedGotData, err := ConvertBits(gotData, 5, 8, false) + if err != nil { + t.Errorf("%q: unexpected convert bits error: %v", test.name, + err) + continue + } + if !bytes.Equal(convertedGotData, data) { + t.Errorf("%q: mismatched data -- got %x, want %x", test.name, + convertedGotData, data) + continue + } + } +} + // TestCanDecodeUnlimtedBech32 tests whether decoding a large bech32 string works // when using the DecodeNoLimit version func TestCanDecodeUnlimtedBech32(t *testing.T) { @@ -118,7 +207,6 @@ func TestCanDecodeUnlimtedBech32(t *testing.T) { // cycle of a bech32 string. It also reports the allocation count, which we // expect to be 2 for a fully optimized cycle. func BenchmarkEncodeDecodeCycle(b *testing.B) { - // Use a fixed, 49-byte raw data for testing. inputData, err := hex.DecodeString("cbe6365ddbcda9a9915422c3f091c13f8c7b2f263b8d34067bd12c274408473fa764871c9dd51b1bb34873b3473b633ed1") if err != nil { diff --git a/bech32/example_test.go b/bech32/example_test.go index 515f0a9..6a1f696 100644 --- a/bech32/example_test.go +++ b/bech32/example_test.go @@ -45,5 +45,5 @@ func ExampleEncode() { fmt.Println("Encoded Data:", encoded) // Output: - // Encoded Data: customHrp!11111q123jhxapqv3shgcgumastr + // Encoded Data: customhrp!11111q123jhxapqv3shgcgkxpuhe }