From ba3d2b6d8f1eab555f6d662de20cbe9d685f399e Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Mon, 22 Nov 2021 10:58:16 +0100 Subject: [PATCH] backend/btc: refactor pkScript encoding/decoding We use functions from the btcd library to convert addresses to pubKeyScripts and vice versa. While the address type has support for Taproot addresses, the pubkey script functions in btcd don't handle them yet. See also: https://github.com/btcsuite/btcd/pull/1768 To speed up send-to-taproot support, refactor the pubkey script encoding/decoding into separate unit-tested functions, to which we can easily add taproot support in the next commit. --- backend/coins/btc/addresses/address.go | 3 +- backend/coins/btc/transaction.go | 6 +- backend/coins/btc/util/util.go | 25 ++++++ backend/coins/btc/util/util_test.go | 103 +++++++++++++++++++++++++ backend/devices/bitbox02/keystore.go | 27 ++++--- 5 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 backend/coins/btc/util/util_test.go diff --git a/backend/coins/btc/addresses/address.go b/backend/coins/btc/addresses/address.go index a7662cd95d..a754bb54d7 100644 --- a/backend/coins/btc/addresses/address.go +++ b/backend/coins/btc/addresses/address.go @@ -23,6 +23,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/digitalbitbox/bitbox-wallet-app/backend/coins/btc/blockchain" + "github.com/digitalbitbox/bitbox-wallet-app/backend/coins/btc/util" "github.com/digitalbitbox/bitbox-wallet-app/backend/signing" "github.com/sirupsen/logrus" ) @@ -126,7 +127,7 @@ func (address *AccountAddress) isUsed() bool { // PubkeyScript returns the pubkey script of this address. Use this in a tx output to receive funds. func (address *AccountAddress) PubkeyScript() []byte { - script, err := txscript.PayToAddrScript(address.Address) + script, err := util.PkScriptFromAddress(address.Address) if err != nil { address.log.WithError(err).Panic("Failed to get the pubkey script for an address.") } diff --git a/backend/coins/btc/transaction.go b/backend/coins/btc/transaction.go index f70d825d22..cde8027da8 100644 --- a/backend/coins/btc/transaction.go +++ b/backend/coins/btc/transaction.go @@ -20,7 +20,6 @@ import ( "strconv" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/digitalbitbox/bitbox-wallet-app/backend/accounts" @@ -29,6 +28,7 @@ import ( "github.com/digitalbitbox/bitbox-wallet-app/backend/coins/btc/blockchain" "github.com/digitalbitbox/bitbox-wallet-app/backend/coins/btc/maketx" "github.com/digitalbitbox/bitbox-wallet-app/backend/coins/btc/transactions" + "github.com/digitalbitbox/bitbox-wallet-app/backend/coins/btc/util" "github.com/digitalbitbox/bitbox-wallet-app/backend/coins/coin" "github.com/digitalbitbox/bitbox-wallet-app/util/errp" ) @@ -80,9 +80,9 @@ func (account *Account) newTx(args *accounts.TxProposalArgs) ( if err != nil { return nil, nil, err } - pkScript, err := txscript.PayToAddrScript(address) + pkScript, err := util.PkScriptFromAddress(address) if err != nil { - return nil, nil, errp.WithStack(err) + return nil, nil, err } utxo := account.transactions.SpendableOutputs() wireUTXO := make(map[wire.OutPoint]maketx.UTXO, len(utxo)) diff --git a/backend/coins/btc/util/util.go b/backend/coins/btc/util/util.go index c8273dc8f4..e92e4f9e5b 100644 --- a/backend/coins/btc/util/util.go +++ b/backend/coins/btc/util/util.go @@ -18,8 +18,11 @@ import ( "strconv" "strings" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" "github.com/digitalbitbox/bitbox-wallet-app/util/errp" ) @@ -39,3 +42,25 @@ func ParseOutPoint(outPointBytes []byte) (*wire.OutPoint, error) { } return wire.NewOutPoint(txHash, uint32(index)), nil } + +// PkScriptFromAddress decodes an address into the pubKeyScript that can be used in a transaction +// output. +func PkScriptFromAddress(address btcutil.Address) ([]byte, error) { + pkScript, err := txscript.PayToAddrScript(address) + if err != nil { + return nil, errp.WithStack(err) + } + return pkScript, nil +} + +// AddressFromPkScript decodes a pkScript into an Address instance. +func AddressFromPkScript(pkScript []byte, net *chaincfg.Params) (btcutil.Address, error) { + _, addresses, _, err := txscript.ExtractPkScriptAddrs(pkScript, net) + if err != nil { + return nil, errp.WithStack(err) + } + if len(addresses) != 1 { + return nil, errp.New("couldn't parse pkScript") + } + return addresses[0], nil +} diff --git a/backend/coins/btc/util/util_test.go b/backend/coins/btc/util/util_test.go new file mode 100644 index 0000000000..40652e16c6 --- /dev/null +++ b/backend/coins/btc/util/util_test.go @@ -0,0 +1,103 @@ +// Copyright 2021 Shift Crypto AG +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/stretchr/testify/require" +) + +func TestPkScriptFromAddress(t *testing.T) { + hash := []byte("\x92\x95\x3b\x69\x91\x29\x70\x02\xfa\xa6\x2a\x1d\xd2\x43\x13\xff\x62\x1e\x10\xab") + net := &chaincfg.MainNetParams + + var address btcutil.Address + + address, err := btcutil.NewAddressPubKeyHash(hash, net) + require.NoError(t, err) + pkScript, err := PkScriptFromAddress(address) + require.NoError(t, err) + require.Equal(t, + []byte("\x76\xa9\x14\x92\x95\x3b\x69\x91\x29\x70\x02\xfa\xa6\x2a\x1d\xd2\x43\x13\xff\x62\x1e\x10\xab\x88\xac"), + pkScript) + + address, err = btcutil.NewAddressWitnessPubKeyHash(hash, net) + require.NoError(t, err) + pkScript, err = PkScriptFromAddress(address) + require.NoError(t, err) + require.Equal(t, + []byte("\x00\x14\x92\x95\x3b\x69\x91\x29\x70\x02\xfa\xa6\x2a\x1d\xd2\x43\x13\xff\x62\x1e\x10\xab"), + pkScript) + + address, err = btcutil.NewAddressScriptHashFromHash(hash, net) + require.NoError(t, err) + pkScript, err = PkScriptFromAddress(address) + require.NoError(t, err) + require.Equal(t, + []byte("\xa9\x14\x92\x95\x3b\x69\x91\x29\x70\x02\xfa\xa6\x2a\x1d\xd2\x43\x13\xff\x62\x1e\x10\xab\x87"), + pkScript) + + scriptHash := []byte("\x4a\xf2\xe4\x54\x9a\x5c\xbb\x73\x6e\x77\xce\xf5\x2f\xe3\x0b\x9d\xf8\x12\x1d\x73\x56\xab\x20\x05\x46\x3e\xcb\x08\x97\x23\x45\x8d") + address, err = btcutil.NewAddressWitnessScriptHash(scriptHash, net) + require.NoError(t, err) + pkScript, err = PkScriptFromAddress(address) + require.NoError(t, err) + require.Equal(t, + []byte("\x00\x20\x4a\xf2\xe4\x54\x9a\x5c\xbb\x73\x6e\x77\xce\xf5\x2f\xe3\x0b\x9d\xf8\x12\x1d\x73\x56\xab\x20\x05\x46\x3e\xcb\x08\x97\x23\x45\x8d"), + pkScript) +} + +func TestAddressFromPkScript(t *testing.T) { + hash := []byte("\x92\x95\x3b\x69\x91\x29\x70\x02\xfa\xa6\x2a\x1d\xd2\x43\x13\xff\x62\x1e\x10\xab") + net := &chaincfg.MainNetParams + + var address btcutil.Address + + address, err := btcutil.NewAddressPubKeyHash(hash, net) + require.NoError(t, err) + pkScript, err := PkScriptFromAddress(address) + require.NoError(t, err) + recoveredAddres, err := AddressFromPkScript(pkScript, &chaincfg.MainNetParams) + require.NoError(t, err) + require.Equal(t, address.ScriptAddress(), recoveredAddres.ScriptAddress()) + + address, err = btcutil.NewAddressWitnessPubKeyHash(hash, net) + require.NoError(t, err) + pkScript, err = PkScriptFromAddress(address) + require.NoError(t, err) + recoveredAddres, err = AddressFromPkScript(pkScript, &chaincfg.MainNetParams) + require.NoError(t, err) + require.Equal(t, address.ScriptAddress(), recoveredAddres.ScriptAddress()) + + address, err = btcutil.NewAddressScriptHashFromHash(hash, net) + require.NoError(t, err) + pkScript, err = PkScriptFromAddress(address) + require.NoError(t, err) + recoveredAddres, err = AddressFromPkScript(pkScript, &chaincfg.MainNetParams) + require.NoError(t, err) + require.Equal(t, address.ScriptAddress(), recoveredAddres.ScriptAddress()) + + scriptHash := []byte("\x4a\xf2\xe4\x54\x9a\x5c\xbb\x73\x6e\x77\xce\xf5\x2f\xe3\x0b\x9d\xf8\x12\x1d\x73\x56\xab\x20\x05\x46\x3e\xcb\x08\x97\x23\x45\x8d") + address, err = btcutil.NewAddressWitnessScriptHash(scriptHash, net) + require.NoError(t, err) + pkScript, err = PkScriptFromAddress(address) + require.NoError(t, err) + recoveredAddres, err = AddressFromPkScript(pkScript, &chaincfg.MainNetParams) + require.NoError(t, err) + require.Equal(t, address.ScriptAddress(), recoveredAddres.ScriptAddress()) +} diff --git a/backend/devices/bitbox02/keystore.go b/backend/devices/bitbox02/keystore.go index 8944a5c7e3..98b2fb7896 100644 --- a/backend/devices/bitbox02/keystore.go +++ b/backend/devices/bitbox02/keystore.go @@ -22,9 +22,10 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" "github.com/digitalbitbox/bitbox-wallet-app/backend/coins/btc" + "github.com/digitalbitbox/bitbox-wallet-app/backend/coins/btc/util" "github.com/digitalbitbox/bitbox-wallet-app/backend/coins/coin" coinpkg "github.com/digitalbitbox/bitbox-wallet-app/backend/coins/coin" "github.com/digitalbitbox/bitbox-wallet-app/backend/coins/eth" @@ -351,16 +352,22 @@ func (keystore *keystore) signBTCTransaction(btcProposedTx *btc.ProposedTransact } outputs := make([]*messages.BTCSignOutputRequest, len(tx.TxOut)) for index, txOut := range tx.TxOut { - scriptClass, addresses, _, err := txscript.ExtractPkScriptAddrs(txOut.PkScript, coin.Net()) + address, err := util.AddressFromPkScript(txOut.PkScript, coin.Net()) if err != nil { - return errp.WithStack(err) - } - if len(addresses) != 1 { - return errp.New("couldn't parse pkScript") + return err } - msgOutputType, ok := btcMsgOutputTypeMap[scriptClass] - if !ok { - return errp.Newf("unsupported output type: %d", scriptClass) + var msgOutputType messages.BTCOutputType + switch address.(type) { + case *btcutil.AddressPubKeyHash: + msgOutputType = messages.BTCOutputType_P2PKH + case *btcutil.AddressScriptHash: + msgOutputType = messages.BTCOutputType_P2SH + case *btcutil.AddressWitnessPubKeyHash: + msgOutputType = messages.BTCOutputType_P2WPKH + case *btcutil.AddressWitnessScriptHash: + msgOutputType = messages.BTCOutputType_P2WSH + default: + return errp.Newf("unsupported output type: %v", address) } changeAddress := btcProposedTx.TXProposal.ChangeAddress isChange := changeAddress != nil && bytes.Equal( @@ -375,7 +382,7 @@ func (keystore *keystore) signBTCTransaction(btcProposedTx *btc.ProposedTransact Ours: isChange, Type: msgOutputType, Value: uint64(txOut.Value), - Payload: addresses[0].ScriptAddress(), + Payload: address.ScriptAddress(), Keypath: keypath, } }