From 49c4bbff3569a5f617d8fffb3defe22d18ffd07f Mon Sep 17 00:00:00 2001 From: Andrew Gaffney Date: Sun, 4 Jun 2023 16:40:41 -0500 Subject: [PATCH] feat: properly handle addresses in ledger This includes some small vendored libraries for base58/bech32 encoding Fixes #290 --- cmd/block-fetch/main.go | 2 +- internal/base58/alphabet.go | 52 ++++ internal/base58/base58.go | 145 +++++++++++ internal/base58/base58check.go | 55 ++++ internal/base58/doc.go | 32 +++ internal/base58/genalphabet.go | 83 ++++++ internal/bech32/bech32.go | 445 +++++++++++++++++++++++++++++++++ internal/bech32/doc.go | 18 ++ internal/bech32/error.go | 90 +++++++ internal/bech32/version.go | 46 ++++ ledger/alonzo.go | 4 +- ledger/babbage.go | 4 +- ledger/common.go | 96 +++++++ ledger/mary.go | 4 +- ledger/shelley.go | 4 +- ledger/tx.go | 2 +- networks.go | 4 +- 17 files changed, 1074 insertions(+), 12 deletions(-) create mode 100644 internal/base58/alphabet.go create mode 100644 internal/base58/base58.go create mode 100644 internal/base58/base58check.go create mode 100644 internal/base58/doc.go create mode 100644 internal/base58/genalphabet.go create mode 100644 internal/bech32/bech32.go create mode 100644 internal/bech32/doc.go create mode 100644 internal/bech32/error.go create mode 100644 internal/bech32/version.go diff --git a/cmd/block-fetch/main.go b/cmd/block-fetch/main.go index 09759eef..f8afe35c 100644 --- a/cmd/block-fetch/main.go +++ b/cmd/block-fetch/main.go @@ -96,7 +96,7 @@ func main() { } fmt.Printf(" Outputs:\n") for _, output := range tx.Outputs() { - fmt.Printf(" Address: %x\n", output.Address()) + fmt.Printf(" Address: %s\n", output.Address()) fmt.Printf(" Amount: %d\n", output.Amount()) assets := output.Assets() if assets != nil { diff --git a/internal/base58/alphabet.go b/internal/base58/alphabet.go new file mode 100644 index 00000000..f228b155 --- /dev/null +++ b/internal/base58/alphabet.go @@ -0,0 +1,52 @@ +// Copyright (c) 2015 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +// NOTE: this file was copied from: +// https://github.com/btcsuite/btcd/tree/v0.23.4/btcutil/base58 + +// AUTOGENERATED by genalphabet.go; do not edit. + +package base58 + +const ( + // alphabet is the modified base58 alphabet used by Bitcoin. + alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + alphabetIdx0 = '1' +) + +var b58 = [256]byte{ + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 255, 255, 255, 255, 255, 255, + 255, 9, 10, 11, 12, 13, 14, 15, + 16, 255, 17, 18, 19, 20, 21, 255, + 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 255, 255, 255, 255, 255, + 255, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 255, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, +} diff --git a/internal/base58/base58.go b/internal/base58/base58.go new file mode 100644 index 00000000..8791ef59 --- /dev/null +++ b/internal/base58/base58.go @@ -0,0 +1,145 @@ +// Copyright (c) 2013-2015 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +// NOTE: this file was copied from: +// https://github.com/btcsuite/btcd/tree/v0.23.4/btcutil/base58 + +package base58 + +import ( + "math/big" +) + +//go:generate go run genalphabet.go + +var bigRadix = [...]*big.Int{ + big.NewInt(0), + big.NewInt(58), + big.NewInt(58 * 58), + big.NewInt(58 * 58 * 58), + big.NewInt(58 * 58 * 58 * 58), + big.NewInt(58 * 58 * 58 * 58 * 58), + big.NewInt(58 * 58 * 58 * 58 * 58 * 58), + big.NewInt(58 * 58 * 58 * 58 * 58 * 58 * 58), + big.NewInt(58 * 58 * 58 * 58 * 58 * 58 * 58 * 58), + big.NewInt(58 * 58 * 58 * 58 * 58 * 58 * 58 * 58 * 58), + bigRadix10, +} + +var bigRadix10 = big.NewInt(58 * 58 * 58 * 58 * 58 * 58 * 58 * 58 * 58 * 58) // 58^10 + +// Decode decodes a modified base58 string to a byte slice. +func Decode(b string) []byte { + answer := big.NewInt(0) + scratch := new(big.Int) + + // Calculating with big.Int is slow for each iteration. + // x += b58[b[i]] * j + // j *= 58 + // + // Instead we can try to do as much calculations on int64. + // We can represent a 10 digit base58 number using an int64. + // + // Hence we'll try to convert 10, base58 digits at a time. + // The rough idea is to calculate `t`, such that: + // + // t := b58[b[i+9]] * 58^9 ... + b58[b[i+1]] * 58^1 + b58[b[i]] * 58^0 + // x *= 58^10 + // x += t + // + // Of course, in addition, we'll need to handle boundary condition when `b` is not multiple of 58^10. + // In that case we'll use the bigRadix[n] lookup for the appropriate power. + for t := b; len(t) > 0; { + n := len(t) + if n > 10 { + n = 10 + } + + total := uint64(0) + for _, v := range t[:n] { + if v > 255 { + return []byte("") + } + + tmp := b58[v] + if tmp == 255 { + return []byte("") + } + total = total*58 + uint64(tmp) + } + + answer.Mul(answer, bigRadix[n]) + scratch.SetUint64(total) + answer.Add(answer, scratch) + + t = t[n:] + } + + tmpval := answer.Bytes() + + var numZeros int + for numZeros = 0; numZeros < len(b); numZeros++ { + if b[numZeros] != alphabetIdx0 { + break + } + } + flen := numZeros + len(tmpval) + val := make([]byte, flen) + copy(val[numZeros:], tmpval) + + return val +} + +// Encode encodes a byte slice to a modified base58 string. +func Encode(b []byte) string { + x := new(big.Int) + x.SetBytes(b) + + // maximum length of output is log58(2^(8*len(b))) == len(b) * 8 / log(58) + maxlen := int(float64(len(b))*1.365658237309761) + 1 + answer := make([]byte, 0, maxlen) + mod := new(big.Int) + for x.Sign() > 0 { + // Calculating with big.Int is slow for each iteration. + // x, mod = x / 58, x % 58 + // + // Instead we can try to do as much calculations on int64. + // x, mod = x / 58^10, x % 58^10 + // + // Which will give us mod, which is 10 digit base58 number. + // We'll loop that 10 times to convert to the answer. + + x.DivMod(x, bigRadix10, mod) + if x.Sign() == 0 { + // When x = 0, we need to ensure we don't add any extra zeros. + m := mod.Int64() + for m > 0 { + answer = append(answer, alphabet[m%58]) + m /= 58 + } + } else { + m := mod.Int64() + for i := 0; i < 10; i++ { + answer = append(answer, alphabet[m%58]) + m /= 58 + } + } + } + + // leading zero bytes + for _, i := range b { + if i != 0 { + break + } + answer = append(answer, alphabetIdx0) + } + + // reverse + alen := len(answer) + for i := 0; i < alen/2; i++ { + answer[i], answer[alen-1-i] = answer[alen-1-i], answer[i] + } + + return string(answer) +} diff --git a/internal/base58/base58check.go b/internal/base58/base58check.go new file mode 100644 index 00000000..dd18ba90 --- /dev/null +++ b/internal/base58/base58check.go @@ -0,0 +1,55 @@ +// Copyright (c) 2013-2014 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +// NOTE: this file was copied from: +// https://github.com/btcsuite/btcd/tree/v0.23.4/btcutil/base58 + +package base58 + +import ( + "crypto/sha256" + "errors" +) + +// ErrChecksum indicates that the checksum of a check-encoded string does not verify against +// the checksum. +var ErrChecksum = errors.New("checksum error") + +// ErrInvalidFormat indicates that the check-encoded string has an invalid format. +var ErrInvalidFormat = errors.New("invalid format: version and/or checksum bytes missing") + +// checksum: first four bytes of sha256^2 +func checksum(input []byte) (cksum [4]byte) { + h := sha256.Sum256(input) + h2 := sha256.Sum256(h[:]) + copy(cksum[:], h2[:4]) + return +} + +// CheckEncode prepends a version byte and appends a four byte checksum. +func CheckEncode(input []byte, version byte) string { + b := make([]byte, 0, 1+len(input)+4) + b = append(b, version) + b = append(b, input...) + cksum := checksum(b) + b = append(b, cksum[:]...) + return Encode(b) +} + +// CheckDecode decodes a string that was encoded with CheckEncode and verifies the checksum. +func CheckDecode(input string) (result []byte, version byte, err error) { + decoded := Decode(input) + if len(decoded) < 5 { + return nil, 0, ErrInvalidFormat + } + version = decoded[0] + var cksum [4]byte + copy(cksum[:], decoded[len(decoded)-4:]) + if checksum(decoded[:len(decoded)-4]) != cksum { + return nil, 0, ErrChecksum + } + payload := decoded[1 : len(decoded)-4] + result = append(result, payload...) + return +} diff --git a/internal/base58/doc.go b/internal/base58/doc.go new file mode 100644 index 00000000..bd9061bd --- /dev/null +++ b/internal/base58/doc.go @@ -0,0 +1,32 @@ +// Copyright (c) 2014 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +// NOTE: this file was copied from: +// https://github.com/btcsuite/btcd/tree/v0.23.4/btcutil/base58 + +/* +Package base58 provides an API for working with modified base58 and Base58Check +encodings. + +Modified Base58 Encoding + +Standard base58 encoding is similar to standard base64 encoding except, as the +name implies, it uses a 58 character alphabet which results in an alphanumeric +string and allows some characters which are problematic for humans to be +excluded. Due to this, there can be various base58 alphabets. + +The modified base58 alphabet used by Bitcoin, and hence this package, omits the +0, O, I, and l characters that look the same in many fonts and are therefore +hard to humans to distinguish. + +Base58Check Encoding Scheme + +The Base58Check encoding scheme is primarily used for Bitcoin addresses at the +time of this writing, however it can be used to generically encode arbitrary +byte arrays into human-readable strings along with a version byte that can be +used to differentiate the same payload. For Bitcoin addresses, the extra +version is used to differentiate the network of otherwise identical public keys +which helps prevent using an address intended for one network on another. +*/ +package base58 diff --git a/internal/base58/genalphabet.go b/internal/base58/genalphabet.go new file mode 100644 index 00000000..1d777839 --- /dev/null +++ b/internal/base58/genalphabet.go @@ -0,0 +1,83 @@ +// Copyright (c) 2015 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +// NOTE: this file was copied from: +// https://github.com/btcsuite/btcd/tree/v0.23.4/btcutil/base58 + +//go:build ignore +// +build ignore + +package main + +import ( + "bytes" + "io" + "log" + "os" + "strconv" +) + +var ( + start = []byte(`// Copyright (c) 2015 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +// AUTOGENERATED by genalphabet.go; do not edit. + +package base58 + +const ( + // alphabet is the modified base58 alphabet used by Bitcoin. + alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + alphabetIdx0 = '1' +) + +var b58 = [256]byte{`) + + end = []byte(`}`) + + alphabet = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") + tab = []byte("\t") + invalid = []byte("255") + comma = []byte(",") + space = []byte(" ") + nl = []byte("\n") +) + +func write(w io.Writer, b []byte) { + _, err := w.Write(b) + if err != nil { + log.Fatal(err) + } +} + +func main() { + fi, err := os.Create("alphabet.go") + if err != nil { + log.Fatal(err) + } + defer fi.Close() + + write(fi, start) + write(fi, nl) + for i := byte(0); i < 32; i++ { + write(fi, tab) + for j := byte(0); j < 8; j++ { + idx := bytes.IndexByte(alphabet, i*8+j) + if idx == -1 { + write(fi, invalid) + } else { + write(fi, strconv.AppendInt(nil, int64(idx), 10)) + } + write(fi, comma) + if j != 7 { + write(fi, space) + } + } + write(fi, nl) + } + write(fi, end) + write(fi, nl) +} diff --git a/internal/bech32/bech32.go b/internal/bech32/bech32.go new file mode 100644 index 00000000..5c09102f --- /dev/null +++ b/internal/bech32/bech32.go @@ -0,0 +1,445 @@ +// 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. + +// NOTE: this file was copied from: +// https://github.com/btcsuite/btcd/tree/v0.23.4/btcutil/bech32 + +package bech32 + +import ( + "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} + +// 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)) + } + 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] + } + } + } + + // 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] + } + } + + // 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, + version Version) { + + bech32Const := int(VersionToConsts[version]) + polymod := bech32Polymod(hrp, data, nil) ^ bech32Const + 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. The version of bech32 used (bech32 OG, or +// bech32m) is also returned to allow the caller to perform proper address +// validation (segwitv0 should use bech32, v1+ should use bech32m). +// +// 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) (Version, bool) { + checksum := data[len(data)-6:] + values := data[:len(data)-6] + polymod := bech32Polymod(hrp, values, checksum) + + // Before BIP-350, we'd always check this against a static constant of + // 1 to know if the checksum was computed properly. As we want to + // generically support decoding for bech32m as well as bech32, we'll + // look up the returned value and compare it to the set of defined + // constants. + bech32Version, ok := ConstsToVersion[ChecksumConst(polymod)] + if ok { + return bech32Version, true + } + + return VersionUnknown, false +} + +// DecodeNoLimit is a bech32 checksum version aware arbitrary string length +// decoder. This function will return the version of the decoded checksum +// constant so higher level validation can be performed to ensure the correct +// version of bech32 was used when encoding. +func decodeNoLimit(bech string) (string, []byte, Version, 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, VersionUnknown, 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, VersionUnknown, 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, VersionUnknown, 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'). + one := strings.LastIndexByte(bech, '1') + if one < 1 || one+7 > len(bech) { + return "", nil, VersionUnknown, ErrInvalidSeparatorIndex(one) + } + + // The human-readable part is everything before the last '1'. + hrp := bech[:one] + data := bech[one+1:] + + // Each character corresponds to the byte with value of the index in + // 'charset'. + decoded, err := toBytes(data) + if err != nil { + return "", nil, VersionUnknown, err + } + + // Verify if the checksum (stored inside decoded[:]) is valid, given the + // previously decoded hrp. + bech32Version, ok := bech32VerifyChecksum(hrp, decoded) + if !ok { + // 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. We'll actually compute _both_ possibly valid checksum + // to further aide in debugging. + var expectedBldr strings.Builder + expectedBldr.Grow(6) + writeBech32Checksum(hrp, payload, &expectedBldr, Version0) + expectedVersion0 := expectedBldr.String() + + var b strings.Builder + b.Grow(6) + writeBech32Checksum(hrp, payload, &expectedBldr, VersionM) + expectedVersionM := expectedBldr.String() + + err = ErrInvalidChecksum{ + Expected: expectedVersion0, + ExpectedM: expectedVersionM, + Actual: actual, + } + return "", nil, VersionUnknown, err + } + + // We exclude the last 6 bytes, which is the checksum. + return hrp, decoded[:len(decoded)-6], bech32Version, nil +} + +// 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 and the human-readable +// part will be lowercase. +func DecodeNoLimit(bech string) (string, []byte, error) { + hrp, data, _, err := decodeNoLimit(bech) + return hrp, data, err +} + +// 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 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 { + return "", nil, ErrInvalidLength(len(bech)) + } + + hrp, data, _, err := decodeNoLimit(bech) + return hrp, data, err +} + +// DecodeGeneric is identical to the existing Decode method, but will also +// return bech32 version that matches the decoded checksum. This method should +// be used when decoding segwit addresses, as it enables additional +// verification to ensure the proper checksum is used. +func DecodeGeneric(bech string) (string, []byte, Version, error) { + // The maximum allowed length for a bech32 string is 90. + if len(bech) > 90 { + return "", nil, VersionUnknown, ErrInvalidLength(len(bech)) + } + + return decodeNoLimit(bech) +} + +// encodeGeneric is the base bech32 encoding function that is aware of the +// existence of the checksum versions. This method is private, as the Encode +// and EncodeM methods are intended to be used instead. +func encodeGeneric(hrp string, data []byte, + version Version) (string, error) { + + // 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) + bldr.WriteString("1") + + // Write the data part, using the bech32 charset. + for _, b := range data { + if int(b) >= len(charset) { + return "", ErrInvalidDataByte(b) + } + bldr.WriteByte(charset[b]) + } + + // Calculate and write the checksum of the data. + writeBech32Checksum(hrp, data, &bldr, version) + + return bldr.String(), nil +} + +// 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) { + return encodeGeneric(hrp, data, Version0) +} + +// EncodeM is the exactly same as the Encode method, but it uses the new +// bech32m constant instead of the original one. It should be used whenever one +// attempts to encode a segwit address of v1 and beyond. +func EncodeM(hrp string, data []byte) (string, error) { + return encodeGeneric(hrp, data, VersionM) +} + +// 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, 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. + 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. + nextByte := byte(0) + filledBits := uint8(0) + + for _, b := range data { + + // Discard unused bits. + b <<= 8 - fromBits + + // How many bits remaining to extract from the input data. + remFromBits := fromBits + for remFromBits > 0 { + // How many bits remaining to be added to the next byte. + remToBits := toBits - filledBits + + // The number of bytes to next extract is the minimum of + // remFromBits and remToBits. + toExtract := remFromBits + if remToBits < toExtract { + toExtract = remToBits + } + + // Add the next bits to nextByte, shifting the already + // added bits to the left. + nextByte = (nextByte << toExtract) | (b >> (8 - toExtract)) + + // Discard the bits we just extracted and get ready for + // next iteration. + b <<= toExtract + remFromBits -= toExtract + filledBits += toExtract + + // If the nextByte is completely filled, we add it to + // our regrouped bytes and start on the next byte. + if filledBits == toBits { + regrouped = append(regrouped, nextByte) + filledBits = 0 + nextByte = 0 + } + } + } + + // We pad any unfinished group if specified. + if pad && filledBits > 0 { + nextByte <<= toBits - filledBits + regrouped = append(regrouped, nextByte) + filledBits = 0 + nextByte = 0 + } + + // Any incomplete group must be <= 4 bits, and all zeroes. + if filledBits > 0 && (filledBits > 4 || nextByte != 0) { + return nil, ErrInvalidIncompleteGroup{} + } + + return regrouped, nil +} + +// EncodeFromBase256 converts a base256-encoded byte slice into a base32-encoded +// byte slice and then encodes it 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. +func EncodeFromBase256(hrp string, data []byte) (string, error) { + converted, err := ConvertBits(data, 8, 5, true) + if err != nil { + return "", err + } + return Encode(hrp, converted) +} + +// DecodeToBase256 decodes a bech32-encoded string into its associated +// human-readable part (HRP) and base32-encoded data, converts that data to a +// base256-encoded byte slice and returns it along with the lowercase HRP. +func DecodeToBase256(bech string) (string, []byte, error) { + hrp, data, err := Decode(bech) + if err != nil { + return "", nil, err + } + converted, err := ConvertBits(data, 5, 8, false) + if err != nil { + return "", nil, err + } + return hrp, converted, nil +} diff --git a/internal/bech32/doc.go b/internal/bech32/doc.go new file mode 100644 index 00000000..1c330aa0 --- /dev/null +++ b/internal/bech32/doc.go @@ -0,0 +1,18 @@ +// Copyright (c) 2017 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +// NOTE: this file was copied from: +// https://github.com/btcsuite/btcd/tree/v0.23.4/btcutil/bech32 + +/* +Package bech32 provides a Go implementation of the bech32 format specified in +BIP 173. + +Bech32 strings consist of a human-readable part (hrp), followed by the +separator 1, then a checksummed data part encoded using the 32 characters +"qpzry9x8gf2tvdw0s3jn54khce6mua7l". + +More info: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki +*/ +package bech32 diff --git a/internal/bech32/error.go b/internal/bech32/error.go new file mode 100644 index 00000000..033e9801 --- /dev/null +++ b/internal/bech32/error.go @@ -0,0 +1,90 @@ +// 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. + +// NOTE: this file was copied from: +// https://github.com/btcsuite/btcd/tree/v0.23.4/btcutil/bech32 + +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. Both the original version, as well as +// the new bech32m checksum may be specified. +type ErrInvalidChecksum struct { + Expected string + ExpectedM string + Actual string +} + +func (e ErrInvalidChecksum) Error() string { + return fmt.Sprintf("invalid checksum (expected (bech32=%v, "+ + "bech32m=%v), got %v)", e.Expected, e.ExpectedM, 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)) +} diff --git a/internal/bech32/version.go b/internal/bech32/version.go new file mode 100644 index 00000000..f44fd028 --- /dev/null +++ b/internal/bech32/version.go @@ -0,0 +1,46 @@ +// NOTE: this file was copied from: +// https://github.com/btcsuite/btcd/tree/v0.23.4/btcutil/bech32 + +package bech32 + +// ChecksumConst is a type that represents the currently defined bech32 +// checksum constants. +type ChecksumConst int + +const ( + // Version0Const is the original constant used in the checksum + // verification for bech32. + Version0Const ChecksumConst = 1 + + // VersionMConst is the new constant used for bech32m checksum + // verification. + VersionMConst ChecksumConst = 0x2bc830a3 +) + +// Version defines the current set of bech32 versions. +type Version uint8 + +const ( + // Version0 defines the original bech version. + Version0 Version = iota + + // VersionM is the new bech32 version defined in BIP-350, also known as + // bech32m. + VersionM + + // VersionUnknown denotes an unknown bech version. + VersionUnknown +) + +// VersionToConsts maps bech32 versions to the checksum constant to be used +// when encoding, and asserting a particular version when decoding. +var VersionToConsts = map[Version]ChecksumConst{ + Version0: Version0Const, + VersionM: VersionMConst, +} + +// ConstsToVersion maps a bech32 constant to the version it's associated with. +var ConstsToVersion = map[ChecksumConst]Version{ + Version0Const: Version0, + VersionMConst: VersionM, +} diff --git a/ledger/alonzo.go b/ledger/alonzo.go index 89430bbe..1a5a32b3 100644 --- a/ledger/alonzo.go +++ b/ledger/alonzo.go @@ -102,7 +102,7 @@ func (b *AlonzoTransactionBody) Outputs() []TransactionOutput { type AlonzoTransactionOutput struct { cbor.StructAsArray cbor.DecodeStoreCbor - OutputAddress []byte + OutputAddress Address OutputAmount MaryTransactionOutputValue DatumHash Blake2b256 } @@ -120,7 +120,7 @@ func (o *AlonzoTransactionOutput) UnmarshalCBOR(cborData []byte) error { return nil } -func (o AlonzoTransactionOutput) Address() []byte { +func (o AlonzoTransactionOutput) Address() Address { return o.OutputAddress } diff --git a/ledger/babbage.go b/ledger/babbage.go index 79b263bc..0c41d143 100644 --- a/ledger/babbage.go +++ b/ledger/babbage.go @@ -144,7 +144,7 @@ func (b *BabbageTransactionBody) Outputs() []TransactionOutput { } type BabbageTransactionOutput struct { - OutputAddress []byte `cbor:"0,keyasint,omitempty"` + OutputAddress Address `cbor:"0,keyasint,omitempty"` OutputAmount MaryTransactionOutputValue `cbor:"1,keyasint,omitempty"` DatumOption []cbor.RawMessage `cbor:"2,keyasint,omitempty"` ScriptRef cbor.Tag `cbor:"3,keyasint,omitempty"` @@ -165,7 +165,7 @@ func (o *BabbageTransactionOutput) UnmarshalCBOR(cborData []byte) error { return nil } -func (o BabbageTransactionOutput) Address() []byte { +func (o BabbageTransactionOutput) Address() Address { return o.OutputAddress } diff --git a/ledger/common.go b/ledger/common.go index 49a31dcb..47bb30f7 100644 --- a/ledger/common.go +++ b/ledger/common.go @@ -15,7 +15,11 @@ package ledger import ( + "fmt" + "github.com/blinklabs-io/gouroboros/cbor" + "github.com/blinklabs-io/gouroboros/internal/base58" + "github.com/blinklabs-io/gouroboros/internal/bech32" ) // MultiAsset represents a collection of policies, assets, and quantities. It's used for @@ -60,3 +64,95 @@ func (m *MultiAsset[T]) Asset(policyId Blake2b224, assetName []byte) T { } return policy[cbor.ByteString(assetName)] } + +const ( + addressHeaderTypeMask = 0xF0 + addressHeaderNetworkMask = 0x0F + addressHashSize = 28 + + addressTypeKeyKey = 0b0000 + addressTypeScriptKey = 0b0001 + addressTypeKeyScript = 0b0010 + addressTypeScriptScript = 0b0011 + addressTypeKeyPointer = 0b0100 + addressTypeScriptPointer = 0b0101 + addressTypeKeyNone = 0b0110 + addressTypeScriptNone = 0b0111 + addressTypeByron = 0b1000 + addressTypeNoneKey = 0b1110 + addressTypeNoneScript = 0b1111 +) + +type Address struct { + addressType uint8 + networkId uint8 + paymentAddress []byte + stakingAddress []byte + hrp string +} + +func (a *Address) UnmarshalCBOR(data []byte) error { + // Decode bytes from CBOR + tmpData := []byte{} + if _, err := cbor.Decode(data, &tmpData); err != nil { + return err + } + // Extract header info + header := tmpData[0] + a.addressType = (header & addressHeaderTypeMask) >> 4 + a.networkId = header & addressHeaderNetworkMask + // Extract payload + // NOTE: this is probably incorrect for Byron + payload := tmpData[1:] + a.paymentAddress = payload[:addressHashSize] + a.stakingAddress = payload[addressHashSize:] + // Generate human readable part of address for output + a.hrp = a.generateHRP() + return nil +} + +func (a *Address) MarshalCBOR() ([]byte, error) { + return cbor.Encode(a.Bytes()) +} + +func (a *Address) generateHRP() string { + var ret string + if a.addressType == addressTypeNoneKey || a.addressType == addressTypeNoneScript { + ret = "stake" + } else { + ret = "addr" + } + // Add test_ suffix if not mainnet + if a.networkId != 1 { + ret += "_test" + } + return ret +} + +func (a *Address) Bytes() []byte { + ret := []byte{} + ret = append(ret, (byte(a.addressType)<<4)|(byte(a.networkId)&addressHeaderNetworkMask)) + ret = append(ret, a.paymentAddress...) + ret = append(ret, a.stakingAddress...) + return ret +} + +func (a Address) String() string { + data := a.Bytes() + if a.addressType == addressTypeByron { + // Encode data to base58 + encoded := base58.Encode(data) + return encoded + } else { + // Convert data to base32 and encode as bech32 + convData, err := bech32.ConvertBits(data, 8, 5, true) + if err != nil { + panic(fmt.Sprintf("unexpected error converting data to base32: %s", err)) + } + encoded, err := bech32.Encode(a.hrp, convData) + if err != nil { + panic(fmt.Sprintf("unexpected error encoding data as bech32: %s", err)) + } + return encoded + } +} diff --git a/ledger/mary.go b/ledger/mary.go index 508031b2..ea409cc2 100644 --- a/ledger/mary.go +++ b/ledger/mary.go @@ -104,11 +104,11 @@ type MaryTransaction struct { type MaryTransactionOutput struct { cbor.StructAsArray - OutputAddress []byte + OutputAddress Address OutputAmount MaryTransactionOutputValue } -func (o MaryTransactionOutput) Address() []byte { +func (o MaryTransactionOutput) Address() Address { return o.OutputAddress } diff --git a/ledger/shelley.go b/ledger/shelley.go index b5c0e51c..ccac7cc0 100644 --- a/ledger/shelley.go +++ b/ledger/shelley.go @@ -181,11 +181,11 @@ func (i ShelleyTransactionInput) Index() uint32 { type ShelleyTransactionOutput struct { cbor.StructAsArray - OutputAddress []byte + OutputAddress Address OutputAmount uint64 } -func (o ShelleyTransactionOutput) Address() []byte { +func (o ShelleyTransactionOutput) Address() Address { return o.OutputAddress } diff --git a/ledger/tx.go b/ledger/tx.go index 08496e01..7b386fc5 100644 --- a/ledger/tx.go +++ b/ledger/tx.go @@ -34,7 +34,7 @@ type TransactionInput interface { } type TransactionOutput interface { - Address() []byte + Address() Address Amount() uint64 Assets() *MultiAsset[uint64] } diff --git a/networks.go b/networks.go index 781cacc0..549e94fc 100644 --- a/networks.go +++ b/networks.go @@ -38,7 +38,7 @@ func NetworkByName(name string) Network { } // NetworkById returns a predefined network by ID -func NetworkById(id uint) Network { +func NetworkById(id uint8) Network { for _, network := range networks { if network.Id == id { return network @@ -59,7 +59,7 @@ func NetworkByNetworkMagic(networkMagic uint32) Network { // Network represents a Cardano network type Network struct { - Id uint + Id uint8 Name string NetworkMagic uint32 PublicRootAddress string