From f281d151bb6fdfbeb88079151d8f747fb90b404c Mon Sep 17 00:00:00 2001 From: Anirudha Bose Date: Tue, 14 Jul 2020 12:12:06 +0530 Subject: [PATCH] bech32: back port improvements from decred/dcrd@9b88dd0 This commit brings a host of improvements to the bech32 package. The public interface of the package remains unchanged. Summary of changes: * Improved error handling using dedicated error types. Programmatically detect if the errors produced are the expected ones. * Improve test coverage to test more corner cases. Added test vectors from Bitcoin Core. * Add a benchmark for a full encode/decode cycle of a bech32 string. * Add a new function DecodeNoLimit, for decoding large bech32 encoded strings. It does NOT validate against the BIP-173 maximum length allowed for bech32 strings. * Automatically convert the HRP to lowercase in Encode function. * Improve performance of encode/decode functions by using strings.Builder. * Improve memory allocation in ConvertBits function. * Updated documentation. Credits: @matheusd Closes #152 and #168. --- bech32/bech32.go | 357 ++++++++++++++++++++++++++---------------- bech32/bech32_test.go | 305 ++++++++++++++++++++++++++++++++---- bech32/error.go | 85 ++++++++++ 3 files changed, 586 insertions(+), 161 deletions(-) create mode 100644 bech32/error.go diff --git a/bech32/bech32.go b/bech32/bech32.go index 8bda0a1..c18fe74 100644 --- a/bech32/bech32.go +++ b/bech32/bech32.go @@ -1,54 +1,196 @@ // Copyright (c) 2017 The btcsuite developers +// Copyright (c) 2019 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package bech32 import ( - "fmt" "strings" ) +// charset is the set of characters used in the data section of bech32 strings. +// Note that this is ordered, such that for a given charset[i], i is the binary +// value of the character. const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +// gen encodes the generator polynomial for the bech32 BCH checksum. var gen = []int{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3} -// Decode decodes a bech32 encoded string, returning the human-readable -// part and the data part excluding the checksum. -func Decode(bech string) (string, []byte, error) { - // The maximum allowed length for a bech32 string is 90. It must also - // be at least 8 characters, since it needs a non-empty HRP, a - // separator, and a 6 character checksum. - if len(bech) < 8 || len(bech) > 90 { - return "", nil, fmt.Errorf("invalid bech32 string length %d", - len(bech)) +// toBytes converts each character in the string 'chars' to the value of the +// index of the correspoding character in 'charset'. +func toBytes(chars string) ([]byte, error) { + decoded := make([]byte, 0, len(chars)) + for i := 0; i < len(chars); i++ { + index := strings.IndexByte(charset, chars[i]) + if index < 0 { + return nil, ErrNonCharsetChar(chars[i]) + } + decoded = append(decoded, byte(index)) } - // Only ASCII characters between 33 and 126 are allowed. - for i := 0; i < len(bech); i++ { - if bech[i] < 33 || bech[i] > 126 { - return "", nil, fmt.Errorf("invalid character in "+ - "string: '%c'", bech[i]) + return decoded, nil +} + +// bech32Polymod calculates the BCH checksum for a given hrp, values and +// checksum data. Checksum is optional, and if nil a 0 checksum is assumed. +// +// Values and checksum (if provided) MUST be encoded as 5 bits per element (base +// 32), otherwise the results are undefined. +// +// For more details on the polymod calculation, please refer to BIP 173. +func bech32Polymod(hrp string, values, checksum []byte) int { + chk := 1 + + // Account for the high bits of the HRP in the checksum. + for i := 0; i < len(hrp); i++ { + b := chk >> 25 + hiBits := int(hrp[i]) >> 5 + chk = (chk&0x1ffffff)<<5 ^ hiBits + for i := 0; i < 5; i++ { + if (b>>uint(i))&1 == 1 { + chk ^= gen[i] + } } } - // The characters must be either all lowercase or all uppercase. - lower := strings.ToLower(bech) - upper := strings.ToUpper(bech) - if bech != lower && bech != upper { - return "", nil, fmt.Errorf("string not all lowercase or all " + - "uppercase") + // Account for the separator (0) between high and low bits of the HRP. + // x^0 == x, so we eliminate the redundant xor used in the other rounds. + b := chk >> 25 + chk = (chk & 0x1ffffff) << 5 + for i := 0; i < 5; i++ { + if (b>>uint(i))&1 == 1 { + chk ^= gen[i] + } } - // We'll work with the lowercase string from now on. - bech = lower + // Account for the low bits of the HRP. + for i := 0; i < len(hrp); i++ { + b := chk >> 25 + loBits := int(hrp[i]) & 31 + chk = (chk&0x1ffffff)<<5 ^ loBits + for i := 0; i < 5; i++ { + if (b>>uint(i))&1 == 1 { + chk ^= gen[i] + } + } + } + + // Account for the values. + for _, v := range values { + b := chk >> 25 + chk = (chk&0x1ffffff)<<5 ^ int(v) + for i := 0; i < 5; i++ { + if (b>>uint(i))&1 == 1 { + chk ^= gen[i] + } + } + } + + if checksum == nil { + // A nil checksum is used during encoding, so assume all bytes are zero. + // x^0 == x, so we eliminate the redundant xor used in the other rounds. + for v := 0; v < 6; v++ { + b := chk >> 25 + chk = (chk & 0x1ffffff) << 5 + for i := 0; i < 5; i++ { + if (b>>uint(i))&1 == 1 { + chk ^= gen[i] + } + } + } + } else { + // Checksum is provided during decoding, so use it. + for _, v := range checksum { + b := chk >> 25 + chk = (chk&0x1ffffff)<<5 ^ int(v) + for i := 0; i < 5; i++ { + if (b>>uint(i))&1 == 1 { + chk ^= gen[i] + } + } + } + } + + return chk +} + +// writeBech32Checksum calculates the checksum data expected for a string that +// will have the given hrp and payload data and writes it to the provided string +// builder. +// +// The payload data MUST be encoded as a base 32 (5 bits per element) byte slice +// and the hrp MUST only use the allowed character set (ascii chars between 33 +// and 126), otherwise the results are undefined. +// +// For more details on the checksum calculation, please refer to BIP 173. +func writeBech32Checksum(hrp string, data []byte, bldr *strings.Builder) { + polymod := bech32Polymod(hrp, data, nil) ^ 1 + for i := 0; i < 6; i++ { + b := byte((polymod >> uint(5*(5-i))) & 31) + + // This can't fail, given we explicitly cap the previous b byte by the + // first 31 bits. + c := charset[b] + bldr.WriteByte(c) + } +} + +// bech32VerifyChecksum verifies whether the bech32 string specified by the +// provided hrp and payload data (encoded as 5 bits per element byte slice) has +// the correct checksum suffix. +// +// Data MUST have more than 6 elements, otherwise this function panics. +// +// For more details on the checksum verification, please refer to BIP 173. +func bech32VerifyChecksum(hrp string, data []byte) bool { + checksum := data[len(data)-6:] + values := data[:len(data)-6] + polymod := bech32Polymod(hrp, values, checksum) + return polymod == 1 +} + +// DecodeNoLimit decodes a bech32 encoded string, returning the human-readable +// 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. +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. + if len(bech) < 8 { + return "", nil, ErrInvalidLength(len(bech)) + } + + // Only ASCII characters between 33 and 126 are allowed. + var hasLower, hasUpper bool + for i := 0; i < len(bech); i++ { + if bech[i] < 33 || bech[i] > 126 { + return "", nil, ErrInvalidCharacter(bech[i]) + } + + // The characters must be either all lowercase or all uppercase. Testing + // directly with ascii codes is safe here, given the previous test. + hasLower = hasLower || (bech[i] >= 97 && bech[i] <= 122) + hasUpper = hasUpper || (bech[i] >= 65 && bech[i] <= 90) + if hasLower && hasUpper { + return "", nil, ErrMixedCase{} + } + } + + // Bech32 standard uses only the lowercase for of strings for checksum + // calculation. + if hasUpper { + bech = strings.ToLower(bech) + } // The string is invalid if the last '1' is non-existent, it is the // first character of the string (no human-readable part) or one of the - // last 6 characters of the string (since checksum cannot contain '1'), - // or if the string is more than 90 characters in total. + // last 6 characters of the string (since checksum cannot contain '1'). one := strings.LastIndexByte(bech, '1') if one < 1 || one+7 > len(bech) { - return "", nil, fmt.Errorf("invalid index of 1") + return "", nil, ErrInvalidSeparatorIndex(one) } // The human-readable part is everything before the last '1'. @@ -59,85 +201,94 @@ func Decode(bech string) (string, []byte, error) { // 'charset'. decoded, err := toBytes(data) if err != nil { - return "", nil, fmt.Errorf("failed converting data to bytes: "+ - "%v", err) + return "", nil, err } + // Verify if the checksum (stored inside decoded[:]) is valid, given the + // previously decoded hrp. if !bech32VerifyChecksum(hrp, decoded) { - moreInfo := "" - checksum := bech[len(bech)-6:] - expected, err := toChars(bech32Checksum(hrp, - decoded[:len(decoded)-6])) - if err == nil { - moreInfo = fmt.Sprintf("Expected %v, got %v.", - expected, checksum) + // Invalid checksum. Calculate what it should have been, so that the + // error contains this information. + + // Extract the payload bytes and actual checksum in the string. + actual := bech[len(bech)-6:] + payload := decoded[:len(decoded)-6] + + // Calculate the expected checksum, given the hrp and payload data. + var expectedBldr strings.Builder + expectedBldr.Grow(6) + writeBech32Checksum(hrp, payload, &expectedBldr) + expected := expectedBldr.String() + + err = ErrInvalidChecksum{ + Expected: expected, + Actual: actual, } - return "", nil, fmt.Errorf("checksum failed. " + moreInfo) + return "", nil, err } // We exclude the last 6 bytes, which is the checksum. return hrp, decoded[:len(decoded)-6], nil } +// 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. +func Decode(bech string) (string, []byte, error) { + // The maximum allowed length for a bech32 string is 90. + if len(bech) > 90 { + return "", nil, ErrInvalidLength(len(bech)) + } + + 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). func Encode(hrp string, data []byte) (string, error) { - // Calculate the checksum of the data and append it at the end. - checksum := bech32Checksum(hrp, data) - combined := append(data, checksum...) // The resulting bech32 string is the concatenation of the hrp, the - // separator 1, data and checksum. Everything after the separator is - // represented using the specified charset. - dataChars, err := toChars(combined) - if err != nil { - return "", fmt.Errorf("unable to convert data bytes to chars: "+ - "%v", err) - } - return hrp + "1" + dataChars, nil -} + // separator 1, data and the 6-byte checksum. + var bldr strings.Builder + bldr.Grow(len(hrp) + 1 + len(data) + 6) + bldr.WriteString(hrp) + bldr.WriteString("1") -// toBytes converts each character in the string 'chars' to the value of the -// index of the correspoding character in 'charset'. -func toBytes(chars string) ([]byte, error) { - decoded := make([]byte, 0, len(chars)) - for i := 0; i < len(chars); i++ { - index := strings.IndexByte(charset, chars[i]) - if index < 0 { - return nil, fmt.Errorf("invalid character not part of "+ - "charset: %v", chars[i]) - } - decoded = append(decoded, byte(index)) - } - return decoded, nil -} - -// toChars converts the byte slice 'data' to a string where each byte in 'data' -// encodes the index of a character in 'charset'. -func toChars(data []byte) (string, error) { - result := make([]byte, 0, len(data)) + // Write the data part, using the bech32 charset. for _, b := range data { if int(b) >= len(charset) { - return "", fmt.Errorf("invalid data byte: %v", b) + return "", ErrInvalidDataByte(b) } - result = append(result, charset[b]) + bldr.WriteByte(charset[b]) } - return string(result), nil + + // Calculate and write the checksum of the data. + writeBech32Checksum(hrp, data, &bldr) + + return bldr.String(), nil } // ConvertBits converts a byte slice where each byte is encoding fromBits bits, // to a byte slice where each byte is encoding toBits bits. func ConvertBits(data []byte, fromBits, toBits uint8, pad bool) ([]byte, error) { if fromBits < 1 || fromBits > 8 || toBits < 1 || toBits > 8 { - return nil, fmt.Errorf("only bit groups between 1 and 8 allowed") + return nil, ErrInvalidBitGroups{} } + // Determine the maximum size the resulting array can have after base + // conversion, so that we can size it a single time. This might be off + // by a byte depending on whether padding is used or not and if the input + // data is a multiple of both fromBits and toBits, but we ignore that and + // just size it to the maximum possible. + maxSize := len(data)*int(fromBits)/int(toBits) + 1 + // The final bytes, each byte encoding toBits bits. - var regrouped []byte + regrouped := make([]byte, 0, maxSize) // Keep track of the next byte we create and how many bits we have - // added to it out of the toBits goal. + // added to it out of the toBits goal. nextByte := byte(0) filledBits := uint8(0) @@ -170,7 +321,7 @@ func ConvertBits(data []byte, fromBits, toBits uint8, pad bool) ([]byte, error) filledBits += toExtract // If the nextByte is completely filled, we add it to - // our regrouped bytes and start on the next byte. + // our regrouped bytes and start on the next byte. if filledBits == toBits { regrouped = append(regrouped, nextByte) filledBits = 0 @@ -189,64 +340,8 @@ func ConvertBits(data []byte, fromBits, toBits uint8, pad bool) ([]byte, error) // Any incomplete group must be <= 4 bits, and all zeroes. if filledBits > 0 && (filledBits > 4 || nextByte != 0) { - return nil, fmt.Errorf("invalid incomplete group") + return nil, ErrInvalidIncompleteGroup{} } return regrouped, nil } - -// For more details on the checksum calculation, please refer to BIP 173. -func bech32Checksum(hrp string, data []byte) []byte { - // Convert the bytes to list of integers, as this is needed for the - // checksum calculation. - integers := make([]int, len(data)) - for i, b := range data { - integers[i] = int(b) - } - values := append(bech32HrpExpand(hrp), integers...) - values = append(values, []int{0, 0, 0, 0, 0, 0}...) - polymod := bech32Polymod(values) ^ 1 - var res []byte - for i := 0; i < 6; i++ { - res = append(res, byte((polymod>>uint(5*(5-i)))&31)) - } - return res -} - -// For more details on the polymod calculation, please refer to BIP 173. -func bech32Polymod(values []int) int { - chk := 1 - for _, v := range values { - b := chk >> 25 - chk = (chk&0x1ffffff)<<5 ^ v - for i := 0; i < 5; i++ { - if (b>>uint(i))&1 == 1 { - chk ^= gen[i] - } - } - } - return chk -} - -// For more details on HRP expansion, please refer to BIP 173. -func bech32HrpExpand(hrp string) []int { - v := make([]int, 0, len(hrp)*2+1) - for i := 0; i < len(hrp); i++ { - v = append(v, int(hrp[i]>>5)) - } - v = append(v, 0) - for i := 0; i < len(hrp); i++ { - v = append(v, int(hrp[i]&31)) - } - return v -} - -// For more details on the checksum verification, please refer to BIP 173. -func bech32VerifyChecksum(hrp string, data []byte) bool { - integers := make([]int, len(data)) - for i, b := range data { - integers[i] = int(b) - } - concat := append(bech32HrpExpand(hrp), integers...) - return bech32Polymod(concat) == 1 -} diff --git a/bech32/bech32_test.go b/bech32/bech32_test.go index f28e028..af4392d 100644 --- a/bech32/bech32_test.go +++ b/bech32/bech32_test.go @@ -1,54 +1,72 @@ -// Copyright (c) 2017 The btcsuite developers +// Copyright (c) 2017-2020 The btcsuite developers +// Copyright (c) 2019 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package bech32_test +package bech32 import ( + "bytes" + "encoding/hex" + "fmt" "strings" "testing" - - "github.com/btcsuite/btcutil/bech32" ) +// TestBech32 tests whether decoding and re-encoding the valid BIP-173 test +// vectors works and if decoding invalid test vectors fails for the correct +// reason. func TestBech32(t *testing.T) { tests := []struct { - str string - valid bool + str string + expectedError error }{ - {"A12UEL5L", true}, - {"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", true}, - {"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", true}, - {"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", true}, - {"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", true}, - {"split1checkupstagehandshakeupstreamerranterredcaperred2y9e2w", false}, // invalid checksum - {"s lit1checkupstagehandshakeupstreamerranterredcaperredp8hs2p", false}, // invalid character (space) in hrp - {"spl\x7Ft1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false}, // invalid character (DEL) in hrp - {"split1cheo2y9e2w", false}, // invalid character (o) in data part - {"split1a2y9w", false}, // too short data part - {"1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false}, // empty hrp - {"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", false}, // too long + {"A12UEL5L", nil}, + {"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", nil}, + {"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", nil}, + {"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", nil}, + {"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", nil}, + {"split1checkupstagehandshakeupstreamerranterredcaperred2y9e2w", ErrInvalidChecksum{"2y9e3w", "2y9e2w"}}, // invalid checksum + {"s lit1checkupstagehandshakeupstreamerranterredcaperredp8hs2p", ErrInvalidCharacter(' ')}, // invalid character (space) in hrp + {"spl\x7Ft1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", ErrInvalidCharacter(127)}, // invalid character (DEL) in hrp + {"split1cheo2y9e2w", ErrNonCharsetChar('o')}, // invalid character (o) in data part + {"split1a2y9w", ErrInvalidSeparatorIndex(5)}, // too short data part + {"1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", ErrInvalidSeparatorIndex(0)}, // empty hrp + {"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", ErrInvalidLength(91)}, // too long + + // Additional test vectors used in bitcoin core + {" 1nwldj5", ErrInvalidCharacter(' ')}, + {"\x7f" + "1axkwrx", ErrInvalidCharacter(0x7f)}, + {"\x801eym55h", ErrInvalidCharacter(0x80)}, + {"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", ErrInvalidLength(91)}, + {"pzry9x0s0muk", ErrInvalidSeparatorIndex(-1)}, + {"1pzry9x0s0muk", ErrInvalidSeparatorIndex(0)}, + {"x1b4n0q5v", ErrNonCharsetChar(98)}, + {"li1dgmt3", ErrInvalidSeparatorIndex(2)}, + {"de1lg7wt\xff", ErrInvalidCharacter(0xff)}, + {"A1G7SGD8", ErrInvalidChecksum{"2uel5l", "g7sgd8"}}, + {"10a06t8", ErrInvalidLength(7)}, + {"1qzzfhee", ErrInvalidSeparatorIndex(0)}, + {"a12UEL5L", ErrMixedCase{}}, + {"A12uEL5L", ErrMixedCase{}}, } - for _, test := range tests { + for i, test := range tests { str := test.str - hrp, decoded, err := bech32.Decode(str) - if !test.valid { - // Invalid string decoding should result in error. - if err == nil { - t.Error("expected decoding to fail for "+ - "invalid string %v", test.str) - } + hrp, decoded, err := Decode(str) + if test.expectedError != err { + t.Errorf("%d: expected decoding error %v "+ + "instead got %v", i, test.expectedError, err) continue } - // Valid string decoding should result in no error. if err != nil { - t.Errorf("expected string to be valid bech32: %v", err) + // End test case here if a decoding error was expected. + continue } // Check that it encodes to the same string - encoded, err := bech32.Encode(hrp, decoded) + encoded, err := Encode(hrp, decoded) if err != nil { t.Errorf("encoding failed: %v", err) } @@ -61,9 +79,236 @@ func TestBech32(t *testing.T) { // Flip a bit in the string an make sure it is caught. pos := strings.LastIndexAny(str, "1") flipped := str[:pos+1] + string((str[pos+1] ^ 1)) + str[pos+2:] - _, _, err = bech32.Decode(flipped) + _, _, err = Decode(flipped) if err == nil { t.Error("expected decoding to fail") } } } + +// TestCanDecodeUnlimtedBech32 tests whether decoding a large bech32 string works +// when using the DecodeNoLimit version +func TestCanDecodeUnlimtedBech32(t *testing.T) { + input := "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq5kx0yd" + + // Sanity check that an input of this length errors on regular Decode() + _, _, err := Decode(input) + if err == nil { + t.Fatalf("Test vector not appropriate") + } + + // Try and decode it. + hrp, data, err := DecodeNoLimit(input) + if err != nil { + t.Fatalf("Expected decoding of large string to work. Got error: %v", err) + } + + // Verify data for correctness. + if hrp != "1" { + t.Fatalf("Unexpected hrp: %v", hrp) + } + decodedHex := fmt.Sprintf("%x", data) + expected := "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000" + if decodedHex != expected { + t.Fatalf("Unexpected decoded data: %s", decodedHex) + } +} + +// BenchmarkEncodeDecodeCycle performs a benchmark for a full encode/decode +// 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 { + b.Fatalf("failed to initialize input data: %v", err) + } + + // Convert this into a 79-byte, base 32 byte slice. + base32Input, err := ConvertBits(inputData, 8, 5, true) + if err != nil { + b.Fatalf("failed to convert input to 32 bits-per-element: %v", err) + } + + // Use a fixed hrp for the tests. This should generate an encoded bech32 + // string of size 90 (the maximum allowed by BIP-173). + hrp := "bc" + + // Begin the benchmark. Given that we test one roundtrip per iteration + // (that is, one Encode() and one Decode() operation), we expect at most + // 2 allocations per reported test op. + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + str, err := Encode(hrp, base32Input) + if err != nil { + b.Fatalf("failed to encode input: %v", err) + } + + _, _, err = Decode(str) + if err != nil { + b.Fatalf("failed to decode string: %v", err) + } + } +} + +// TestConvertBits tests whether base conversion works using TestConvertBits(). +func TestConvertBits(t *testing.T) { + tests := []struct { + input string + output string + fromBits uint8 + toBits uint8 + pad bool + }{ + // Trivial empty conversions. + {"", "", 8, 5, false}, + {"", "", 8, 5, true}, + {"", "", 5, 8, false}, + {"", "", 5, 8, true}, + + // Conversions of 0 value with/without padding. + {"00", "00", 8, 5, false}, + {"00", "0000", 8, 5, true}, + {"0000", "00", 5, 8, false}, + {"0000", "0000", 5, 8, true}, + + // Testing when conversion ends exactly at the byte edge. This makes + // both padded and unpadded versions the same. + {"0000000000", "0000000000000000", 8, 5, false}, + {"0000000000", "0000000000000000", 8, 5, true}, + {"0000000000000000", "0000000000", 5, 8, false}, + {"0000000000000000", "0000000000", 5, 8, true}, + + // Conversions of full byte sequences. + {"ffffff", "1f1f1f1f1e", 8, 5, true}, + {"1f1f1f1f1e", "ffffff", 5, 8, false}, + {"1f1f1f1f1e", "ffffff00", 5, 8, true}, + + // Sample random conversions. + {"c9ca", "190705", 8, 5, false}, + {"c9ca", "19070500", 8, 5, true}, + {"19070500", "c9ca", 5, 8, false}, + {"19070500", "c9ca00", 5, 8, true}, + + // Test cases tested on TestConvertBitsFailures with their corresponding + // fixes. + {"ff", "1f1c", 8, 5, true}, + {"1f1c10", "ff20", 5, 8, true}, + + // Large conversions. + { + "cbe6365ddbcda9a9915422c3f091c13f8c7b2f263b8d34067bd12c274408473fa764871c9dd51b1bb34873b3473b633ed1", + "190f13030c170e1b1916141a13040a14040b011f01040e01071e0607160b1906070e06130801131b1a0416020e110008081c1f1a0e19040703120e1d0a06181b160d0407070c1a07070d11131d1408", + 8, 5, true, + }, + { + "190f13030c170e1b1916141a13040a14040b011f01040e01071e0607160b1906070e06130801131b1a0416020e110008081c1f1a0e19040703120e1d0a06181b160d0407070c1a07070d11131d1408", + "cbe6365ddbcda9a9915422c3f091c13f8c7b2f263b8d34067bd12c274408473fa764871c9dd51b1bb34873b3473b633ed100", + 5, 8, true, + }, + } + + for i, tc := range tests { + input, err := hex.DecodeString(tc.input) + if err != nil { + t.Fatalf("invalid test input data: %v", err) + } + + expected, err := hex.DecodeString(tc.output) + if err != nil { + t.Fatalf("invalid test output data: %v", err) + } + + actual, err := ConvertBits(input, tc.fromBits, tc.toBits, tc.pad) + if err != nil { + t.Fatalf("test case %d failed: %v", i, err) + } + + if !bytes.Equal(actual, expected) { + t.Fatalf("test case %d has wrong output; expected=%x actual=%x", + i, expected, actual) + } + } +} + +// TestConvertBitsFailures tests for the expected conversion failures of +// ConvertBits(). +func TestConvertBitsFailures(t *testing.T) { + tests := []struct { + input string + fromBits uint8 + toBits uint8 + pad bool + err error + }{ + // Not enough output bytes when not using padding. + {"ff", 8, 5, false, ErrInvalidIncompleteGroup{}}, + {"1f1c10", 5, 8, false, ErrInvalidIncompleteGroup{}}, + + // Unsupported bit conversions. + {"", 0, 5, false, ErrInvalidBitGroups{}}, + {"", 10, 5, false, ErrInvalidBitGroups{}}, + {"", 5, 0, false, ErrInvalidBitGroups{}}, + {"", 5, 10, false, ErrInvalidBitGroups{}}, + } + + for i, tc := range tests { + input, err := hex.DecodeString(tc.input) + if err != nil { + t.Fatalf("invalid test input data: %v", err) + } + + _, err = ConvertBits(input, tc.fromBits, tc.toBits, tc.pad) + if err != tc.err { + t.Fatalf("test case %d failure: expected '%v' got '%v'", i, + tc.err, err) + } + } + +} + +// BenchmarkConvertBitsDown benchmarks the speed and memory allocation behavior +// of ConvertBits when converting from a higher base into a lower base (e.g. 8 +// => 5). +// +// Only a single allocation is expected, which is used for the output array. +func BenchmarkConvertBitsDown(b *testing.B) { + // Use a fixed, 49-byte raw data for testing. + inputData, err := hex.DecodeString("cbe6365ddbcda9a9915422c3f091c13f8c7b2f263b8d34067bd12c274408473fa764871c9dd51b1bb34873b3473b633ed1") + if err != nil { + b.Fatalf("failed to initialize input data: %v", err) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := ConvertBits(inputData, 8, 5, true) + if err != nil { + b.Fatalf("error converting bits: %v", err) + } + } +} + +// BenchmarkConvertBitsDown benchmarks the speed and memory allocation behavior +// of ConvertBits when converting from a lower base into a higher base (e.g. 5 +// => 8). +// +// Only a single allocation is expected, which is used for the output array. +func BenchmarkConvertBitsUp(b *testing.B) { + // Use a fixed, 79-byte raw data for testing. + inputData, err := hex.DecodeString("190f13030c170e1b1916141a13040a14040b011f01040e01071e0607160b1906070e06130801131b1a0416020e110008081c1f1a0e19040703120e1d0a06181b160d0407070c1a07070d11131d1408") + if err != nil { + b.Fatalf("failed to initialize input data: %v", err) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := ConvertBits(inputData, 8, 5, true) + if err != nil { + b.Fatalf("error converting bits: %v", err) + } + } +} diff --git a/bech32/error.go b/bech32/error.go new file mode 100644 index 0000000..c987b6e --- /dev/null +++ b/bech32/error.go @@ -0,0 +1,85 @@ +// Copyright (c) 2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package bech32 + +import ( + "fmt" +) + +// ErrMixedCase is returned when the bech32 string has both lower and uppercase +// characters. +type ErrMixedCase struct{} + +func (e ErrMixedCase) Error() string { + return "string not all lowercase or all uppercase" +} + +// ErrInvalidBitGroups is returned when conversion is attempted between byte +// slices using bit-per-element of unsupported value. +type ErrInvalidBitGroups struct{} + +func (e ErrInvalidBitGroups) Error() string { + return "only bit groups between 1 and 8 allowed" +} + +// ErrInvalidIncompleteGroup is returned when then byte slice used as input has +// data of wrong length. +type ErrInvalidIncompleteGroup struct{} + +func (e ErrInvalidIncompleteGroup) Error() string { + return "invalid incomplete group" +} + +// ErrInvalidLength is returned when the bech32 string has an invalid length +// given the BIP-173 defined restrictions. +type ErrInvalidLength int + +func (e ErrInvalidLength) Error() string { + return fmt.Sprintf("invalid bech32 string length %d", int(e)) +} + +// ErrInvalidCharacter is returned when the bech32 string has a character +// outside the range of the supported charset. +type ErrInvalidCharacter rune + +func (e ErrInvalidCharacter) Error() string { + return fmt.Sprintf("invalid character in string: '%c'", rune(e)) +} + +// ErrInvalidSeparatorIndex is returned when the separator character '1' is +// in an invalid position in the bech32 string. +type ErrInvalidSeparatorIndex int + +func (e ErrInvalidSeparatorIndex) Error() string { + return fmt.Sprintf("invalid separator index %d", int(e)) +} + +// ErrNonCharsetChar is returned when a character outside of the specific +// bech32 charset is used in the string. +type ErrNonCharsetChar rune + +func (e ErrNonCharsetChar) Error() string { + return fmt.Sprintf("invalid character not part of charset: %v", int(e)) +} + +// ErrInvalidChecksum is returned when the extracted checksum of the string +// is different than what was expected. +type ErrInvalidChecksum struct { + Expected string + Actual string +} + +func (e ErrInvalidChecksum) Error() string { + return fmt.Sprintf("invalid checksum (expected %v got %v)", + e.Expected, e.Actual) +} + +// ErrInvalidDataByte is returned when a byte outside the range required for +// conversion into a string was found. +type ErrInvalidDataByte byte + +func (e ErrInvalidDataByte) Error() string { + return fmt.Sprintf("invalid data byte: %v", byte(e)) +}