From 4a0c04f989bc1b1b02763c001667be3052754b57 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 27 Sep 2021 13:40:05 +0200 Subject: [PATCH 1/2] address: refactor common code into AddressSegWit To avoid a lot of code duplication, we extract the common parts of the AddressWitnessPubKeyHash and AddressWitnessScriptHash types into a common base type called AddressSegWit. --- address.go | 205 ++++++++++++++++++++--------------------------- internal_test.go | 16 ++-- 2 files changed, 98 insertions(+), 123 deletions(-) diff --git a/address.go b/address.go index 8672497..96c6050 100644 --- a/address.go +++ b/address.go @@ -506,25 +506,88 @@ func (a *AddressPubKey) PubKey() *btcec.PublicKey { return a.pubKey } +// AddressSegWit is the base address type for all SegWit addresses. +type AddressSegWit struct { + hrp string + witnessVersion byte + witnessProgram []byte +} + +// EncodeAddress returns the bech32 (or bech32m for SegWit v1) string encoding +// of an AddressSegWit. +// +// NOTE: This method is part of the Address interface. +func (a *AddressSegWit) EncodeAddress() string { + str, err := encodeSegWitAddress( + a.hrp, a.witnessVersion, a.witnessProgram[:], + ) + if err != nil { + return "" + } + return str +} + +// ScriptAddress returns the witness program for this address. +// +// NOTE: This method is part of the Address interface. +func (a *AddressSegWit) ScriptAddress() []byte { + return a.witnessProgram[:] +} + +// IsForNet returns whether the AddressSegWit is associated with the passed +// bitcoin network. +// +// NOTE: This method is part of the Address interface. +func (a *AddressSegWit) IsForNet(net *chaincfg.Params) bool { + return a.hrp == net.Bech32HRPSegwit +} + +// String returns a human-readable string for the AddressWitnessPubKeyHash. +// This is equivalent to calling EncodeAddress, but is provided so the type +// can be used as a fmt.Stringer. +// +// NOTE: This method is part of the Address interface. +func (a *AddressSegWit) String() string { + return a.EncodeAddress() +} + +// Hrp returns the human-readable part of the bech32 (or bech32m for SegWit v1) +// encoded AddressSegWit. +func (a *AddressSegWit) Hrp() string { + return a.hrp +} + +// WitnessVersion returns the witness version of the AddressSegWit. +func (a *AddressSegWit) WitnessVersion() byte { + return a.witnessVersion +} + +// WitnessProgram returns the witness program of the AddressSegWit. +func (a *AddressSegWit) WitnessProgram() []byte { + return a.witnessProgram[:] +} + // AddressWitnessPubKeyHash is an Address for a pay-to-witness-pubkey-hash // (P2WPKH) output. See BIP 173 for further details regarding native segregated // witness address encoding: // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki type AddressWitnessPubKeyHash struct { - hrp string - witnessVersion byte - witnessProgram [20]byte + AddressSegWit } // NewAddressWitnessPubKeyHash returns a new AddressWitnessPubKeyHash. -func NewAddressWitnessPubKeyHash(witnessProg []byte, net *chaincfg.Params) (*AddressWitnessPubKeyHash, error) { +func NewAddressWitnessPubKeyHash(witnessProg []byte, + net *chaincfg.Params) (*AddressWitnessPubKeyHash, error) { + return newAddressWitnessPubKeyHash(net.Bech32HRPSegwit, witnessProg) } // newAddressWitnessPubKeyHash is an internal helper function to create an // AddressWitnessPubKeyHash with a known human-readable part, rather than // looking it up through its parameters. -func newAddressWitnessPubKeyHash(hrp string, witnessProg []byte) (*AddressWitnessPubKeyHash, error) { +func newAddressWitnessPubKeyHash(hrp string, + witnessProg []byte) (*AddressWitnessPubKeyHash, error) { + // Check for valid program length for witness version 0, which is 20 // for P2WPKH. if len(witnessProg) != 20 { @@ -533,68 +596,22 @@ func newAddressWitnessPubKeyHash(hrp string, witnessProg []byte) (*AddressWitnes } addr := &AddressWitnessPubKeyHash{ - hrp: strings.ToLower(hrp), - witnessVersion: 0x00, + AddressSegWit{ + hrp: strings.ToLower(hrp), + witnessVersion: 0x00, + witnessProgram: witnessProg, + }, } - copy(addr.witnessProgram[:], witnessProg) - return addr, nil } -// EncodeAddress returns the bech32 string encoding of an -// AddressWitnessPubKeyHash. -// Part of the Address interface. -func (a *AddressWitnessPubKeyHash) EncodeAddress() string { - str, err := encodeSegWitAddress(a.hrp, a.witnessVersion, - a.witnessProgram[:]) - if err != nil { - return "" - } - return str -} - -// ScriptAddress returns the witness program for this address. -// Part of the Address interface. -func (a *AddressWitnessPubKeyHash) ScriptAddress() []byte { - return a.witnessProgram[:] -} - -// IsForNet returns whether or not the AddressWitnessPubKeyHash is associated -// with the passed bitcoin network. -// Part of the Address interface. -func (a *AddressWitnessPubKeyHash) IsForNet(net *chaincfg.Params) bool { - return a.hrp == net.Bech32HRPSegwit -} - -// String returns a human-readable string for the AddressWitnessPubKeyHash. -// This is equivalent to calling EncodeAddress, but is provided so the type -// can be used as a fmt.Stringer. -// Part of the Address interface. -func (a *AddressWitnessPubKeyHash) String() string { - return a.EncodeAddress() -} - -// Hrp returns the human-readable part of the bech32 encoded -// AddressWitnessPubKeyHash. -func (a *AddressWitnessPubKeyHash) Hrp() string { - return a.hrp -} - -// WitnessVersion returns the witness version of the AddressWitnessPubKeyHash. -func (a *AddressWitnessPubKeyHash) WitnessVersion() byte { - return a.witnessVersion -} - -// WitnessProgram returns the witness program of the AddressWitnessPubKeyHash. -func (a *AddressWitnessPubKeyHash) WitnessProgram() []byte { - return a.witnessProgram[:] -} - // Hash160 returns the witness program of the AddressWitnessPubKeyHash as a // byte array. func (a *AddressWitnessPubKeyHash) Hash160() *[20]byte { - return &a.witnessProgram + var pubKeyHashWitnessProgram [20]byte + copy(pubKeyHashWitnessProgram[:], a.witnessProgram) + return &pubKeyHashWitnessProgram } // AddressWitnessScriptHash is an Address for a pay-to-witness-script-hash @@ -602,20 +619,22 @@ func (a *AddressWitnessPubKeyHash) Hash160() *[20]byte { // witness address encoding: // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki type AddressWitnessScriptHash struct { - hrp string - witnessVersion byte - witnessProgram [32]byte + AddressSegWit } // NewAddressWitnessScriptHash returns a new AddressWitnessPubKeyHash. -func NewAddressWitnessScriptHash(witnessProg []byte, net *chaincfg.Params) (*AddressWitnessScriptHash, error) { +func NewAddressWitnessScriptHash(witnessProg []byte, + net *chaincfg.Params) (*AddressWitnessScriptHash, error) { + return newAddressWitnessScriptHash(net.Bech32HRPSegwit, witnessProg) } // newAddressWitnessScriptHash is an internal helper function to create an // AddressWitnessScriptHash with a known human-readable part, rather than // looking it up through its parameters. -func newAddressWitnessScriptHash(hrp string, witnessProg []byte) (*AddressWitnessScriptHash, error) { +func newAddressWitnessScriptHash(hrp string, + witnessProg []byte) (*AddressWitnessScriptHash, error) { + // Check for valid program length for witness version 0, which is 32 // for P2WSH. if len(witnessProg) != 32 { @@ -624,60 +643,12 @@ func newAddressWitnessScriptHash(hrp string, witnessProg []byte) (*AddressWitnes } addr := &AddressWitnessScriptHash{ - hrp: strings.ToLower(hrp), - witnessVersion: 0x00, + AddressSegWit{ + hrp: strings.ToLower(hrp), + witnessVersion: 0x00, + witnessProgram: witnessProg, + }, } - copy(addr.witnessProgram[:], witnessProg) - return addr, nil } - -// EncodeAddress returns the bech32 string encoding of an -// AddressWitnessScriptHash. -// Part of the Address interface. -func (a *AddressWitnessScriptHash) EncodeAddress() string { - str, err := encodeSegWitAddress(a.hrp, a.witnessVersion, - a.witnessProgram[:]) - if err != nil { - return "" - } - return str -} - -// ScriptAddress returns the witness program for this address. -// Part of the Address interface. -func (a *AddressWitnessScriptHash) ScriptAddress() []byte { - return a.witnessProgram[:] -} - -// IsForNet returns whether or not the AddressWitnessScriptHash is associated -// with the passed bitcoin network. -// Part of the Address interface. -func (a *AddressWitnessScriptHash) IsForNet(net *chaincfg.Params) bool { - return a.hrp == net.Bech32HRPSegwit -} - -// String returns a human-readable string for the AddressWitnessScriptHash. -// This is equivalent to calling EncodeAddress, but is provided so the type -// can be used as a fmt.Stringer. -// Part of the Address interface. -func (a *AddressWitnessScriptHash) String() string { - return a.EncodeAddress() -} - -// Hrp returns the human-readable part of the bech32 encoded -// AddressWitnessScriptHash. -func (a *AddressWitnessScriptHash) Hrp() string { - return a.hrp -} - -// WitnessVersion returns the witness version of the AddressWitnessScriptHash. -func (a *AddressWitnessScriptHash) WitnessVersion() byte { - return a.witnessVersion -} - -// WitnessProgram returns the witness program of the AddressWitnessScriptHash. -func (a *AddressWitnessScriptHash) WitnessProgram() []byte { - return a.witnessProgram[:] -} diff --git a/internal_test.go b/internal_test.go index 7462da6..d471c30 100644 --- a/internal_test.go +++ b/internal_test.go @@ -59,9 +59,11 @@ func TstAddressWitnessPubKeyHash(version byte, program [20]byte, hrp string) *AddressWitnessPubKeyHash { return &AddressWitnessPubKeyHash{ - hrp: hrp, - witnessVersion: version, - witnessProgram: program, + AddressSegWit{ + hrp: hrp, + witnessVersion: version, + witnessProgram: program[:], + }, } } @@ -71,9 +73,11 @@ func TstAddressWitnessScriptHash(version byte, program [32]byte, hrp string) *AddressWitnessScriptHash { return &AddressWitnessScriptHash{ - hrp: hrp, - witnessVersion: version, - witnessProgram: program, + AddressSegWit{ + hrp: hrp, + witnessVersion: version, + witnessProgram: program[:], + }, } } From c5b2b0b97977fdeadbd9374410432351b39b5e00 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 27 Sep 2021 13:40:07 +0200 Subject: [PATCH 2/2] address: add AddressTaproot --- address.go | 78 ++++++++++++++++++++++++++++--- address_test.go | 118 +++++++++++++++++++++++++++++++++++++++++++++-- internal_test.go | 29 ++++++++++++ 3 files changed, 216 insertions(+), 9 deletions(-) diff --git a/address.go b/address.go index 96c6050..8cbafea 100644 --- a/address.go +++ b/address.go @@ -64,8 +64,8 @@ func encodeAddress(hash160 []byte, netID byte) string { return base58.CheckEncode(hash160[:ripemd160.Size], netID) } -// encodeSegWitAddress creates a bech32 encoded address string representation -// from witness version and witness program. +// encodeSegWitAddress creates a bech32 (or bech32m for SegWit v1) encoded +// address string representation from witness version and witness program. func encodeSegWitAddress(hrp string, witnessVersion byte, witnessProgram []byte) (string, error) { // Group the address bytes into 5 bit groups, as this is what is used to // encode each character in the address string. @@ -79,7 +79,19 @@ func encodeSegWitAddress(hrp string, witnessVersion byte, witnessProgram []byte) combined := make([]byte, len(converted)+1) combined[0] = witnessVersion copy(combined[1:], converted) - bech, err := bech32.Encode(hrp, combined) + + var bech string + switch witnessVersion { + case 0: + bech, err = bech32.Encode(hrp, combined) + + case 1: + bech, err = bech32.EncodeM(hrp, combined) + + default: + return "", fmt.Errorf("unsupported witness version %d", + witnessVersion) + } if err != nil { return "", err } @@ -149,8 +161,9 @@ func DecodeAddress(addr string, defaultNet *chaincfg.Params) (Address, error) { } // We currently only support P2WPKH and P2WSH, which is - // witness version 0. - if witnessVer != 0 { + // witness version 0 and P2TR which is witness version + // 1. + if witnessVer != 0 && witnessVer != 1 { return nil, UnsupportedWitnessVerError(witnessVer) } @@ -161,6 +174,10 @@ func DecodeAddress(addr string, defaultNet *chaincfg.Params) (Address, error) { case 20: return newAddressWitnessPubKeyHash(hrp, witnessProg) case 32: + if witnessVer == 1 { + return newAddressTaproot(hrp, witnessProg) + } + return newAddressWitnessScriptHash(hrp, witnessProg) default: return nil, UnsupportedWitnessProgLenError(len(witnessProg)) @@ -210,7 +227,7 @@ func DecodeAddress(addr string, defaultNet *chaincfg.Params) (Address, error) { // returns the witness version and witness program byte representation. func decodeSegWitAddress(address string) (byte, []byte, error) { // Decode the bech32 encoded address. - _, data, err := bech32.Decode(address) + _, data, bech32version, err := bech32.DecodeGeneric(address) if err != nil { return 0, nil, err } @@ -246,6 +263,18 @@ func decodeSegWitAddress(address string) (byte, []byte, error) { "version 0: %v", len(regrouped)) } + // For witness version 0, the bech32 encoding must be used. + if version == 0 && bech32version != bech32.Version0 { + return 0, nil, fmt.Errorf("invalid checksum expected bech32 " + + "encoding for address with witness version 0") + } + + // For witness version 1, the bech32m encoding must be used. + if version == 1 && bech32version != bech32.VersionM { + return 0, nil, fmt.Errorf("invalid checksum expected bech32m " + + "encoding for address with witness version 1") + } + return version, regrouped, nil } @@ -652,3 +681,40 @@ func newAddressWitnessScriptHash(hrp string, return addr, nil } + +// AddressTaproot is an Address for a pay-to-taproot (P2TR) output. See BIP 341 +// for further details. +type AddressTaproot struct { + AddressSegWit +} + +// NewAddressTaproot returns a new AddressTaproot. +func NewAddressTaproot(witnessProg []byte, + net *chaincfg.Params) (*AddressTaproot, error) { + + return newAddressTaproot(net.Bech32HRPSegwit, witnessProg) +} + +// newAddressWitnessScriptHash is an internal helper function to create an +// AddressWitnessScriptHash with a known human-readable part, rather than +// looking it up through its parameters. +func newAddressTaproot(hrp string, + witnessProg []byte) (*AddressTaproot, error) { + + // Check for valid program length for witness version 1, which is 32 + // for P2TR. + if len(witnessProg) != 32 { + return nil, errors.New("witness program must be 32 bytes for " + + "p2tr") + } + + addr := &AddressTaproot{ + AddressSegWit{ + hrp: strings.ToLower(hrp), + witnessVersion: 0x01, + witnessProgram: witnessProg, + }, + } + + return addr, nil +} diff --git a/address_test.go b/address_test.go index b6a990d..503ff5c 100644 --- a/address_test.go +++ b/address_test.go @@ -655,13 +655,123 @@ func TestAddresses(t *testing.T) { }, net: &customParams, }, - // Unsupported witness versions (version 0 only supported at this point) + + // P2TR address tests. { - name: "segwit mainnet witness v1", - addr: "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", + name: "segwit v1 mainnet p2tr", + addr: "bc1paardr2nczq0rx5rqpfwnvpzm497zvux64y0f7wjgcs7xuuuh2nnqwr2d5c", + encoded: "bc1paardr2nczq0rx5rqpfwnvpzm497zvux64y0f7wjgcs7xuuuh2nnqwr2d5c", + valid: true, + result: btcutil.TstAddressTaproot( + 1, [32]byte{ + 0xef, 0x46, 0xd1, 0xaa, 0x78, 0x10, 0x1e, 0x33, + 0x50, 0x60, 0x0a, 0x5d, 0x36, 0x04, 0x5b, 0xa9, + 0x7c, 0x26, 0x70, 0xda, 0xa9, 0x1e, 0x9f, 0x3a, + 0x48, 0xc4, 0x3c, 0x6e, 0x73, 0x97, 0x54, 0xe6, + }, chaincfg.MainNetParams.Bech32HRPSegwit, + ), + f: func() (btcutil.Address, error) { + scriptHash := []byte{ + 0xef, 0x46, 0xd1, 0xaa, 0x78, 0x10, 0x1e, 0x33, + 0x50, 0x60, 0x0a, 0x5d, 0x36, 0x04, 0x5b, 0xa9, + 0x7c, 0x26, 0x70, 0xda, 0xa9, 0x1e, 0x9f, 0x3a, + 0x48, 0xc4, 0x3c, 0x6e, 0x73, 0x97, 0x54, 0xe6, + } + return btcutil.NewAddressTaproot( + scriptHash, &chaincfg.MainNetParams, + ) + }, + net: &chaincfg.MainNetParams, + }, + + // Invalid bech32m tests. Source: + // https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki + { + name: "segwit v1 invalid human-readable part", + addr: "tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut", valid: false, net: &chaincfg.MainNetParams, }, + { + name: "segwit v1 mainnet bech32 instead of bech32m", + addr: "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit v1 testnet bech32 instead of bech32m", + addr: "tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf", + valid: false, + net: &chaincfg.TestNet3Params, + }, + { + name: "segwit v1 mainnet bech32 instead of bech32m upper case", + addr: "BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit v0 mainnet bech32m instead of bech32", + addr: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit v1 testnet bech32 instead of bech32m second test", + addr: "tb1q0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq24jc47", + valid: false, + net: &chaincfg.TestNet3Params, + }, + { + name: "segwit v1 mainnet bech32m invalid character in checksum", + addr: "bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit mainnet witness v17", + addr: "BC130XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ7ZWS8R", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit v1 mainnet bech32m invalid program length (1 byte)", + addr: "bc1pw5dgrnzv", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit v1 mainnet bech32m invalid program length (41 bytes)", + addr: "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit v1 testnet bech32m mixed case", + addr: "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq", + valid: false, + net: &chaincfg.TestNet3Params, + }, + { + name: "segwit v1 mainnet bech32m zero padding of more than 4 bits", + addr: "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit v1 mainnet bech32m non-zero padding in 8-to-5-conversion", + addr: "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j", + valid: false, + net: &chaincfg.TestNet3Params, + }, + { + name: "segwit v1 mainnet bech32m empty data section", + addr: "bc1gmk9yu", + valid: false, + net: &chaincfg.MainNetParams, + }, + + // Unsupported witness versions (version 0 and 1 only supported at this point) { name: "segwit mainnet witness v16", addr: "BC1SW50QA3JX3S", @@ -788,6 +898,8 @@ func TestAddresses(t *testing.T) { saddr = btcutil.TstAddressSegwitSAddr(encoded) case *btcutil.AddressWitnessScriptHash: saddr = btcutil.TstAddressSegwitSAddr(encoded) + case *btcutil.AddressTaproot: + saddr = btcutil.TstAddressTaprootSAddr(encoded) } // Check script address, as well as the Hash160 method for P2PKH and diff --git a/internal_test.go b/internal_test.go index d471c30..ec2daa0 100644 --- a/internal_test.go +++ b/internal_test.go @@ -81,6 +81,19 @@ func TstAddressWitnessScriptHash(version byte, program [32]byte, } } +// TstAddressTaproot creates an AddressTaproot, initiating the fields as given. +func TstAddressTaproot(version byte, program [32]byte, + hrp string) *AddressTaproot { + + return &AddressTaproot{ + AddressSegWit{ + hrp: hrp, + witnessVersion: version, + witnessProgram: program[:], + }, + } +} + // TstAddressPubKey makes an AddressPubKey, setting the unexported fields with // the parameters. func TstAddressPubKey(serializedPubKey []byte, pubKeyFormat PubKeyFormat, @@ -116,3 +129,19 @@ func TstAddressSegwitSAddr(addr string) []byte { } return data } + +// TstAddressTaprootSAddr returns the expected witness program bytes for a +// bech32m encoded P2TR bitcoin address. +func TstAddressTaprootSAddr(addr string) []byte { + _, data, err := bech32.Decode(addr) + if err != nil { + return []byte{} + } + + // First byte is version, rest is base 32 encoded data. + data, err = bech32.ConvertBits(data[1:], 5, 8, false) + if err != nil { + return []byte{} + } + return data +}