From c5b2b0b97977fdeadbd9374410432351b39b5e00 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 27 Sep 2021 13:40:07 +0200 Subject: [PATCH] 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 +}