From 66edcae7041319bd1e7be0a903f1b01dbd595daa Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 27 Aug 2020 21:14:53 +0200 Subject: [PATCH 1/6] mod: update psbt library version --- go.mod | 1 + go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/go.mod b/go.mod index aeab90f..a90685e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ require ( github.com/btcsuite/btcd v0.20.1-beta.0.20200513120220-b470eee47728 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d + github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0 github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 github.com/btcsuite/btcwallet/walletdb v1.3.3 diff --git a/go.sum b/go.sum index d0e58fd..3625f4a 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0 h1:3Zumkyl6PWyHuVJ04me0xeD9CnPOhNgeGpapFbzy7O4= +github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= From 314cd98152a6a3941ce9123c0b6d825c6e56e36c Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 27 Aug 2020 21:14:55 +0200 Subject: [PATCH 2/6] wallet: extract addUtxo in create TX test --- wallet/createtx_test.go | 84 ++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/wallet/createtx_test.go b/wallet/createtx_test.go index 04a52c1..31a8753 100644 --- a/wallet/createtx_test.go +++ b/wallet/createtx_test.go @@ -18,6 +18,14 @@ import ( "github.com/btcsuite/btcwallet/wtxmgr" ) +var ( + testBlockHash, _ = chainhash.NewHashFromStr( + "00000000000000017188b968a371bab95aa43522665353b646e41865abae" + + "02a4", + ) + testBlockHeight int32 = 276425 +) + // TestTxToOutput checks that no new address is added to he database if we // request a dry run of the txToOutputs call. It also makes sure a subsequent // non-dry run call produces a similar transaction to the dry-run. @@ -45,41 +53,7 @@ func TestTxToOutputsDryRun(t *testing.T) { txOut, }, } - - var b bytes.Buffer - if err := incomingTx.Serialize(&b); err != nil { - t.Fatalf("unable to serialize tx: %v", err) - } - txBytes := b.Bytes() - - rec, err := wtxmgr.NewTxRecord(txBytes, time.Now()) - if err != nil { - t.Fatalf("unable to create tx record: %v", err) - } - - // The block meta will be inserted to tell the wallet this is a - // confirmed transaction. - blockHash, _ := chainhash.NewHashFromStr( - "00000000000000017188b968a371bab95aa43522665353b646e41865abae02a4") - block := &wtxmgr.BlockMeta{ - Block: wtxmgr.Block{Hash: *blockHash, Height: 276425}, - Time: time.Unix(1387737310, 0), - } - - if err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { - ns := tx.ReadWriteBucket(wtxmgrNamespaceKey) - err = w.TxStore.InsertTx(ns, rec, block) - if err != nil { - return err - } - err = w.TxStore.AddCredit(ns, rec, block, 0, false) - if err != nil { - return err - } - return nil - }); err != nil { - t.Fatalf("failed inserting tx: %v", err) - } + addUtxo(t, w, incomingTx) // Now tell the wallet to create a transaction paying to the specified // outputs. @@ -175,3 +149,43 @@ func TestTxToOutputsDryRun(t *testing.T) { "than wet run") } } + +// addUtxo add the given transaction to the wallet's database marked as a +// confirmed UTXO . +func addUtxo(t *testing.T, w *Wallet, incomingTx *wire.MsgTx) { + var b bytes.Buffer + if err := incomingTx.Serialize(&b); err != nil { + t.Fatalf("unable to serialize tx: %v", err) + } + txBytes := b.Bytes() + + rec, err := wtxmgr.NewTxRecord(txBytes, time.Now()) + if err != nil { + t.Fatalf("unable to create tx record: %v", err) + } + + // The block meta will be inserted to tell the wallet this is a + // confirmed transaction. + block := &wtxmgr.BlockMeta{ + Block: wtxmgr.Block{ + Hash: *testBlockHash, + Height: testBlockHeight, + }, + Time: time.Unix(1387737310, 0), + } + + if err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(wtxmgrNamespaceKey) + err = w.TxStore.InsertTx(ns, rec, block) + if err != nil { + return err + } + err = w.TxStore.AddCredit(ns, rec, block, 0, false) + if err != nil { + return err + } + return nil + }); err != nil { + t.Fatalf("failed inserting tx: %v", err) + } +} From 4aa36af74c2f600ce91bc9d42366d5609eae9c5a Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 27 Aug 2020 21:14:56 +0200 Subject: [PATCH 3/6] wallet: extract addrMgrWithChangeSource --- wallet/createtx.go | 55 +++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/wallet/createtx.go b/wallet/createtx.go index 6bc5aae..0ebc1a1 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -120,7 +120,7 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, } defer dbtx.Rollback() - addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + addrmgrNs, changeSource := w.addrMgrWithChangeSource(dbtx, account) // Get current block's height and hash. bs, err := chainClient.BlockStamp() @@ -134,28 +134,6 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, } inputSource := makeInputSource(eligible) - changeSource := func() ([]byte, error) { - // Derive the change output script. We'll use the default key - // scope responsible for P2WPKH addresses to do so. As a hack to - // allow spending from the imported account, change addresses - // are created from account 0. - var changeAddr btcutil.Address - var err error - changeKeyScope := waddrmgr.KeyScopeBIP0084 - if account == waddrmgr.ImportedAddrAccount { - changeAddr, err = w.newChangeAddress( - addrmgrNs, 0, changeKeyScope, - ) - } else { - changeAddr, err = w.newChangeAddress( - addrmgrNs, account, changeKeyScope, - ) - } - if err != nil { - return nil, err - } - return txscript.PayToAddrScript(changeAddr) - } tx, err = txauthor.NewUnsignedTransaction(outputs, feeSatPerKb, inputSource, changeSource) if err != nil { @@ -270,6 +248,37 @@ func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, account uint32, minco return eligible, nil } +// addrMgrWithChangeSource returns the address manager bucket and a change +// source function that returns change addresses from said address manager. +func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx, + account uint32) (walletdb.ReadWriteBucket, txauthor.ChangeSource) { + + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + changeSource := func() ([]byte, error) { + // Derive the change output script. We'll use the default key + // scope responsible for P2WPKH addresses to do so. As a hack to + // allow spending from the imported account, change addresses + // are created from account 0. + var changeAddr btcutil.Address + var err error + changeKeyScope := waddrmgr.KeyScopeBIP0084 + if account == waddrmgr.ImportedAddrAccount { + changeAddr, err = w.newChangeAddress( + addrmgrNs, 0, changeKeyScope, + ) + } else { + changeAddr, err = w.newChangeAddress( + addrmgrNs, account, changeKeyScope, + ) + } + if err != nil { + return nil, err + } + return txscript.PayToAddrScript(changeAddr) + } + return addrmgrNs, changeSource +} + // validateMsgTx verifies transaction input scripts for tx. All previous output // scripts from outputs redeemed by the transaction, in the same order they are // spent, must be passed in the prevScripts slice. From 1f2ed87055122499be90f0ab91785a02e42eca2b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 27 Aug 2020 21:14:57 +0200 Subject: [PATCH 4/6] wallet: add FetchInputInfo --- wallet/utxos.go | 81 ++++++++++++++++++++++++++++++++++++++++++++ wallet/utxos_test.go | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 wallet/utxos_test.go diff --git a/wallet/utxos.go b/wallet/utxos.go index a754f99..a29d094 100644 --- a/wallet/utxos.go +++ b/wallet/utxos.go @@ -6,11 +6,22 @@ package wallet import ( + "errors" + "fmt" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" ) +var ( + // ErrNotMine is an error denoting that a Wallet instance is unable to + // spend a specified output. + ErrNotMine = errors.New("the passed output does not belong to the " + + "wallet") +) + // OutputSelectionPolicy describes the rules for selecting an output from the // wallet. type OutputSelectionPolicy struct { @@ -88,3 +99,73 @@ func (w *Wallet) UnspentOutputs(policy OutputSelectionPolicy) ([]*TransactionOut }) return outputResults, err } + +// FetchInputInfo queries for the wallet's knowledge of the passed outpoint. If +// the wallet determines this output is under its control, then the original +// full transaction, the target txout and the number of confirmations are +// returned. Otherwise, a non-nil error value of ErrNotMine is returned instead. +func (w *Wallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, + *wire.TxOut, int64, error) { + + // We manually look up the output within the tx store. + txid := &prevOut.Hash + txDetail, err := UnstableAPI(w).TxDetails(txid) + if err != nil { + return nil, nil, 0, err + } else if txDetail == nil { + return nil, nil, 0, ErrNotMine + } + + // With the output retrieved, we'll make an additional check to ensure + // we actually have control of this output. We do this because the check + // above only guarantees that the transaction is somehow relevant to us, + // like in the event of us being the sender of the transaction. + numOutputs := uint32(len(txDetail.TxRecord.MsgTx.TxOut)) + if prevOut.Index >= numOutputs { + return nil, nil, 0, fmt.Errorf("invalid output index %v for "+ + "transaction with %v outputs", prevOut.Index, + numOutputs) + } + pkScript := txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].PkScript + if _, err := w.fetchOutputAddr(pkScript); err != nil { + return nil, nil, 0, err + } + + // Determine the number of confirmations the output currently has. + _, currentHeight, err := w.chainClient.GetBestBlock() + if err != nil { + return nil, nil, 0, fmt.Errorf("unable to retrieve current "+ + "height: %v", err) + } + confs := int64(0) + if txDetail.Block.Height != -1 { + confs = int64(currentHeight - txDetail.Block.Height) + } + + return &txDetail.TxRecord.MsgTx, &wire.TxOut{ + Value: txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].Value, + PkScript: pkScript, + }, confs, nil +} + +// fetchOutputAddr attempts to fetch the managed address corresponding to the +// passed output script. This function is used to look up the proper key which +// should be used to sign a specified input. +func (w *Wallet) fetchOutputAddr(script []byte) (waddrmgr.ManagedAddress, error) { + _, addrs, _, err := txscript.ExtractPkScriptAddrs(script, w.chainParams) + if err != nil { + return nil, err + } + + // If the case of a multi-sig output, several address may be extracted. + // Therefore, we simply select the key for the first address we know + // of. + for _, addr := range addrs { + addr, err := w.AddressInfo(addr) + if err == nil { + return addr, nil + } + } + + return nil, ErrNotMine +} diff --git a/wallet/utxos_test.go b/wallet/utxos_test.go new file mode 100644 index 0000000..3181c80 --- /dev/null +++ b/wallet/utxos_test.go @@ -0,0 +1,61 @@ +// Copyright (c) 2020 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wallet + +import ( + "bytes" + "testing" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/waddrmgr" +) + +// TestFetchInputInfo checks that the wallet can gather information about an +// output based on the address. +func TestFetchInputInfo(t *testing.T) { + w, cleanup := testWallet(t) + defer cleanup() + + // Create an address we can use to send some coins to. + addr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0084) + if err != nil { + t.Fatalf("unable to get current address: %v", addr) + } + p2shAddr, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Fatalf("unable to convert wallet address to p2sh: %v", err) + } + + // Add an output paying to the wallet's address to the database. + utxOut := wire.NewTxOut(100000, p2shAddr) + incomingTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{utxOut}, + } + addUtxo(t, w, incomingTx) + + // Look up the UTXO for the outpoint now and compare it to our + // expectations. + prevOut := &wire.OutPoint{ + Hash: incomingTx.TxHash(), + Index: 0, + } + tx, out, confirmations, err := w.FetchInputInfo(prevOut) + if err != nil { + t.Fatalf("error fetching input info: %v", err) + } + if !bytes.Equal(out.PkScript, utxOut.PkScript) || out.Value != utxOut.Value { + t.Fatalf("unexpected TX out, got %v wanted %v", out, utxOut) + } + if !bytes.Equal(tx.TxOut[prevOut.Index].PkScript, utxOut.PkScript) { + t.Fatalf("unexpected TX out, got %v wanted %v", + tx.TxOut[prevOut.Index].PkScript, utxOut) + } + if confirmations != int64(0-testBlockHeight) { + t.Fatalf("unexpected number of confirmations, got %d wanted %d", + confirmations, 0-testBlockHeight) + } +} From 78d8c81e0aa9bbea8382b1770fe6c2f1893a446b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 27 Aug 2020 21:14:59 +0200 Subject: [PATCH 5/6] wallet: add ComputeInputScript --- wallet/signer.go | 102 +++++++++++++++++++++++++++++++++++++++++ wallet/signer_test.go | 103 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 wallet/signer.go create mode 100644 wallet/signer_test.go diff --git a/wallet/signer.go b/wallet/signer.go new file mode 100644 index 0000000..390022b --- /dev/null +++ b/wallet/signer.go @@ -0,0 +1,102 @@ +// Copyright (c) 2020 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wallet + +import ( + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/waddrmgr" +) + +// PrivKeyTweaker is a function type that can be used to pass in a callback for +// tweaking a private key before it's used to sign an input. +type PrivKeyTweaker func(*btcec.PrivateKey) (*btcec.PrivateKey, error) + +// ComputeInputScript generates a complete InputScript for the passed +// transaction with the signature as defined within the passed SignDescriptor. +// This method is capable of generating the proper input script for both +// regular p2wkh output and p2wkh outputs nested within a regular p2sh output. +func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut, + inputIndex int, sigHashes *txscript.TxSigHashes, + hashType txscript.SigHashType, tweaker PrivKeyTweaker) (wire.TxWitness, + []byte, error) { + + // First make sure we can sign for the input by making sure the script + // in the UTXO belongs to our wallet and we have the private key for it. + walletAddr, err := w.fetchOutputAddr(output.PkScript) + if err != nil { + return nil, nil, err + } + + pka := walletAddr.(waddrmgr.ManagedPubKeyAddress) + privKey, err := pka.PrivKey() + if err != nil { + return nil, nil, err + } + + var ( + witnessProgram []byte + sigScript []byte + ) + + switch { + // If we're spending p2wkh output nested within a p2sh output, then + // we'll need to attach a sigScript in addition to witness data. + case pka.AddrType() == waddrmgr.NestedWitnessPubKey: + pubKey := privKey.PubKey() + pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed()) + + // Next, we'll generate a valid sigScript that will allow us to + // spend the p2sh output. The sigScript will contain only a + // single push of the p2wkh witness program corresponding to + // the matching public key of this address. + p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash( + pubKeyHash, w.chainParams, + ) + if err != nil { + return nil, nil, err + } + witnessProgram, err = txscript.PayToAddrScript(p2wkhAddr) + if err != nil { + return nil, nil, err + } + + bldr := txscript.NewScriptBuilder() + bldr.AddData(witnessProgram) + sigScript, err = bldr.Script() + if err != nil { + return nil, nil, err + } + + // Otherwise, this is a regular p2wkh output, so we include the + // witness program itself as the subscript to generate the proper + // sighash digest. As part of the new sighash digest algorithm, the + // p2wkh witness program will be expanded into a regular p2kh + // script. + default: + witnessProgram = output.PkScript + } + + // If we need to maybe tweak our private key, do it now. + if tweaker != nil { + privKey, err = tweaker(privKey) + if err != nil { + return nil, nil, err + } + } + + // Generate a valid witness stack for the input. + witnessScript, err := txscript.WitnessSignature( + tx, sigHashes, inputIndex, output.Value, witnessProgram, + hashType, privKey, true, + ) + if err != nil { + return nil, nil, err + } + + return witnessScript, sigScript, nil +} diff --git a/wallet/signer_test.go b/wallet/signer_test.go new file mode 100644 index 0000000..0c0cd00 --- /dev/null +++ b/wallet/signer_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2020 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wallet + +import ( + "testing" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/waddrmgr" +) + +// TestComputeInputScript checks that the wallet can create the full +// witness script for a witness output. +func TestComputeInputScript(t *testing.T) { + testCases := []struct { + name string + scope waddrmgr.KeyScope + expectedScriptLen int + }{{ + name: "BIP084 P2WKH", + scope: waddrmgr.KeyScopeBIP0084, + expectedScriptLen: 0, + }, { + name: "BIP049 nested P2WKH", + scope: waddrmgr.KeyScopeBIP0049Plus, + expectedScriptLen: 23, + }} + + w, cleanup := testWallet(t) + defer cleanup() + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + runTestCase(t, w, tc.scope, tc.expectedScriptLen) + }) + } +} + +func runTestCase(t *testing.T, w *Wallet, scope waddrmgr.KeyScope, + scriptLen int) { + + // Create an address we can use to send some coins to. + addr, err := w.CurrentAddress(0, scope) + if err != nil { + t.Fatalf("unable to get current address: %v", addr) + } + p2shAddr, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Fatalf("unable to convert wallet address to p2sh: %v", err) + } + + // Add an output paying to the wallet's address to the database. + utxOut := wire.NewTxOut(100000, p2shAddr) + incomingTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{utxOut}, + } + addUtxo(t, w, incomingTx) + + // Create a transaction that spends the UTXO created above and spends to + // the same address again. + prevOut := wire.OutPoint{ + Hash: incomingTx.TxHash(), + Index: 0, + } + outgoingTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{ + PreviousOutPoint: prevOut, + }}, + TxOut: []*wire.TxOut{utxOut}, + } + sigHashes := txscript.NewTxSigHashes(outgoingTx) + + // Compute the input script to spend the UTXO now. + witness, script, err := w.ComputeInputScript( + outgoingTx, utxOut, 0, sigHashes, txscript.SigHashAll, nil, + ) + if err != nil { + t.Fatalf("error computing input script: %v", err) + } + if len(script) != scriptLen { + t.Fatalf("unexpected script length, got %d wanted %d", + len(script), scriptLen) + } + if len(witness) != 2 { + t.Fatalf("unexpected witness stack length, got %d, wanted %d", + len(witness), 2) + } + + // Finally verify that the created witness is valid. + outgoingTx.TxIn[0].Witness = witness + outgoingTx.TxIn[0].SignatureScript = script + err = validateMsgTx( + outgoingTx, [][]byte{utxOut.PkScript}, []btcutil.Amount{100000}, + ) + if err != nil { + t.Fatalf("error validating tx: %v", err) + } +} From c4c2a9052eec08f3f542abc5b32bf885c7f73ae3 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 27 Aug 2020 21:15:00 +0200 Subject: [PATCH 6/6] wallet: add PSBT funding+finalizing methods --- wallet/psbt.go | 305 ++++++++++++++++++++++++++++++++++ wallet/psbt_test.go | 395 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 700 insertions(+) create mode 100644 wallet/psbt.go create mode 100644 wallet/psbt_test.go diff --git a/wallet/psbt.go b/wallet/psbt.go new file mode 100644 index 0000000..1b5b211 --- /dev/null +++ b/wallet/psbt.go @@ -0,0 +1,305 @@ +// Copyright (c) 2020 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wallet + +import ( + "bytes" + "fmt" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/psbt" + "github.com/btcsuite/btcwallet/wallet/txauthor" + "github.com/btcsuite/btcwallet/wallet/txrules" + "github.com/btcsuite/btcwallet/wtxmgr" +) + +// FundPsbt creates a fully populated PSBT packet that contains enough inputs to +// fund the outputs specified in the passed in packet with the specified fee +// rate. If there is change left, a change output from the wallet is added. +// +// NOTE: If the packet doesn't contain any inputs, coin selection is performed +// automatically. If the packet does contain any inputs, it is assumed that full +// coin selection happened externally and no additional inputs are added. If the +// specified inputs aren't enough to fund the outputs with the given fee rate, +// an error is returned. +// +// NOTE: A caller of the method should hold the global coin selection lock of +// the wallet. However, no UTXO specific lock lease is acquired for any of the +// selected/validated inputs by this method. It is in the caller's +// responsibility to lock the inputs before handing the partial transaction out. +func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32, + feeSatPerKB btcutil.Amount) error { + + // Make sure the packet is well formed. We only require there to be at + // least one output but not necessarily any inputs. + err := psbt.VerifyInputOutputLen(packet, false, true) + if err != nil { + return err + } + + txOut := packet.UnsignedTx.TxOut + txIn := packet.UnsignedTx.TxIn + + // Make sure none of the outputs are dust. + for _, output := range txOut { + // When checking an output for things like dusty-ness, we'll + // use the default mempool relay fee rather than the target + // effective fee rate to ensure accuracy. Otherwise, we may + // mistakenly mark small-ish, but not quite dust output as + // dust. + err := txrules.CheckOutput(output, txrules.DefaultRelayFeePerKb) + if err != nil { + return err + } + } + + // Let's find out the amount to fund first. + amt := int64(0) + for _, output := range txOut { + amt += output.Value + } + + // addInputInfo is a helper function that fetches the UTXO information + // of an input and attaches it to the PSBT packet. + addInputInfo := func(inputs []*wire.TxIn) error { + packet.Inputs = make([]psbt.PInput, len(inputs)) + for idx, in := range inputs { + tx, utxo, _, err := w.FetchInputInfo( + &in.PreviousOutPoint, + ) + if err != nil { + return fmt.Errorf("error fetching UTXO: %v", + err) + } + + // As a fix for CVE-2020-14199 we have to always include + // the full non-witness UTXO in the PSBT for segwit v0. + packet.Inputs[idx].NonWitnessUtxo = tx + + // To make it more obvious that this is actually a + // witness output being spent, we also add the same + // information as the witness UTXO. + packet.Inputs[idx].WitnessUtxo = &wire.TxOut{ + Value: utxo.Value, + PkScript: utxo.PkScript, + } + packet.Inputs[idx].SighashType = txscript.SigHashAll + + // We don't want to include the witness just yet. + packet.UnsignedTx.TxIn[idx].Witness = wire.TxWitness{} + } + + return nil + } + + var tx *txauthor.AuthoredTx + switch { + // We need to do coin selection. + case len(txIn) == 0: + // We ask the underlying wallet to fund a TX for us. This + // includes everything we need, specifically fee estimation and + // change address creation. + tx, err = w.CreateSimpleTx( + account, packet.UnsignedTx.TxOut, 1, feeSatPerKB, + false, + ) + if err != nil { + return fmt.Errorf("error creating funding TX: %v", err) + } + + // Copy over the inputs now then collect all UTXO information + // that we can and attach them to the PSBT as well. We don't + // include the witness as the resulting PSBT isn't expected not + // should be signed yet. + packet.UnsignedTx.TxIn = tx.Tx.TxIn + err = addInputInfo(tx.Tx.TxIn) + if err != nil { + return err + } + + // If there are inputs, we need to check if they're sufficient and add + // a change output if necessary. + default: + // Make sure all inputs provided are actually ours. + err = addInputInfo(txIn) + if err != nil { + return err + } + + // We can leverage the fee calculation of the txauthor package + // if we provide the selected UTXOs as a coin source. + credits := make([]wtxmgr.Credit, len(txIn)) + for idx, in := range txIn { + utxo := packet.Inputs[idx].WitnessUtxo + credits[idx] = wtxmgr.Credit{ + OutPoint: in.PreviousOutPoint, + Amount: btcutil.Amount(utxo.Value), + PkScript: utxo.PkScript, + } + } + inputSource := makeInputSource(credits) + + // We also need a change source which needs to be able to insert + // a new change addresse into the database. + dbtx, err := w.db.BeginReadWriteTx() + if err != nil { + return err + } + _, changeSource := w.addrMgrWithChangeSource(dbtx, account) + + // Ask the txauthor to create a transaction with our selected + // coins. This will perform fee estimation and add a change + // output if necessary. + tx, err = txauthor.NewUnsignedTransaction( + txOut, feeSatPerKB, inputSource, changeSource, + ) + if err != nil { + _ = dbtx.Rollback() + return fmt.Errorf("fee estimation not successful: %v", + err) + } + + // The transaction could be created, let's commit the DB TX to + // store the change address (if one was created). + err = dbtx.Commit() + if err != nil { + return fmt.Errorf("could not add change address to "+ + "database: %v", err) + } + } + + // If there is a change output, we need to copy it over to the PSBT now. + if tx.ChangeIndex >= 0 { + packet.UnsignedTx.TxOut = append( + packet.UnsignedTx.TxOut, + tx.Tx.TxOut[tx.ChangeIndex], + ) + packet.Outputs = append(packet.Outputs, psbt.POutput{}) + } + + // Now that we have the final PSBT ready, we can sort it according to + // BIP 69. This will sort the wire inputs and outputs and move the + // partial inputs and outputs accordingly. + err = psbt.InPlaceSort(packet) + if err != nil { + return fmt.Errorf("could not sort PSBT: %v", err) + } + + return nil +} + +// FinalizePsbt expects a partial transaction with all inputs and outputs fully +// declared and tries to sign all inputs that belong to the wallet. Our wallet +// must be the last signer of the transaction. That means, if there are any +// unsigned non-witness inputs or inputs without UTXO information attached or +// inputs without witness data that do not belong to the wallet, this method +// will fail. If no error is returned, the PSBT is ready to be extracted and the +// final TX within to be broadcast. +// +// NOTE: This method does NOT publish the transaction after it's been finalized +// successfully. +func (w *Wallet) FinalizePsbt(packet *psbt.Packet) error { + // Let's check that this is actually something we can and want to sign. + // We need at least one input and one output. + err := psbt.VerifyInputOutputLen(packet, true, true) + if err != nil { + return err + } + + // Go through each input that doesn't have final witness data attached + // to it already and try to sign it. We do expect that we're the last + // ones to sign. If there is any input without witness data that we + // cannot sign because it's not our UTXO, this will be a hard failure. + tx := packet.UnsignedTx + sigHashes := txscript.NewTxSigHashes(tx) + for idx, txIn := range tx.TxIn { + in := packet.Inputs[idx] + + // We can only sign if we have UTXO information available. We + // can just continue here as a later step will fail with a more + // precise error message. + if in.WitnessUtxo == nil && in.NonWitnessUtxo == nil { + continue + } + + // Skip this input if it's got final witness data attached. + if len(in.FinalScriptWitness) > 0 { + continue + } + + // We can only sign this input if it's ours, so we try to map it + // to a coin we own. If we can't, then we'll continue as it + // isn't our input. + fullTx, txOut, _, err := w.FetchInputInfo( + &txIn.PreviousOutPoint, + ) + if err != nil { + continue + } + + // Find out what UTXO we are signing. Wallets _should_ always + // provide the full non-witness UTXO for segwit v0. + var signOutput *wire.TxOut + if in.NonWitnessUtxo != nil { + prevIndex := txIn.PreviousOutPoint.Index + signOutput = in.NonWitnessUtxo.TxOut[prevIndex] + + if !psbt.TxOutsEqual(txOut, signOutput) { + return fmt.Errorf("found UTXO %#v but it "+ + "doesn't match PSBT's input %v", txOut, + signOutput) + } + + if fullTx.TxHash() != txIn.PreviousOutPoint.Hash { + return fmt.Errorf("found UTXO tx %v but it "+ + "doesn't match PSBT's input %v", + fullTx.TxHash(), + txIn.PreviousOutPoint.Hash) + } + } + + // Fall back to witness UTXO only for older wallets. + if in.WitnessUtxo != nil { + signOutput = in.WitnessUtxo + + if !psbt.TxOutsEqual(txOut, signOutput) { + return fmt.Errorf("found UTXO %#v but it "+ + "doesn't match PSBT's input %v", txOut, + signOutput) + } + } + + // Finally, we'll sign the input as is, and populate the input + // with the witness and sigScript (if needed). + witness, sigScript, err := w.ComputeInputScript( + tx, signOutput, idx, sigHashes, in.SighashType, nil, + ) + if err != nil { + return fmt.Errorf("error computing input script for "+ + "input %d: %v", idx, err) + } + + // Serialize the witness format from the stack representation to + // the wire representation. + var witnessBytes bytes.Buffer + err = psbt.WriteTxWitness(&witnessBytes, witness) + if err != nil { + return fmt.Errorf("error serializing witness: %v", err) + } + packet.Inputs[idx].FinalScriptWitness = witnessBytes.Bytes() + packet.Inputs[idx].FinalScriptSig = sigScript + } + + // Make sure the PSBT itself thinks it's finalized and ready to be + // broadcast. + err = psbt.MaybeFinalizeAll(packet) + if err != nil { + return fmt.Errorf("error finalizing PSBT: %v", err) + } + + return nil +} diff --git a/wallet/psbt_test.go b/wallet/psbt_test.go new file mode 100644 index 0000000..5d77808 --- /dev/null +++ b/wallet/psbt_test.go @@ -0,0 +1,395 @@ +// Copyright (c) 2020 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wallet + +import ( + "bytes" + "encoding/hex" + "strings" + "testing" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/psbt" + "github.com/btcsuite/btcwallet/waddrmgr" +) + +var ( + testScriptP2WSH, _ = hex.DecodeString( + "0020d554616badeb46ccd4ce4b115e1c8d098e942d1387212d0af9ff93a1" + + "9c8f100e", + ) + testScriptP2WKH, _ = hex.DecodeString( + "0014e7a43aa41ef6d72dc6baeeaad8362cedf63b79a3", + ) +) + +// TestFundPsbt tests that a given PSBT packet is funded correctly. +func TestFundPsbt(t *testing.T) { + w, cleanup := testWallet(t) + defer cleanup() + + // Create a P2WKH address we can use to send some coins to. + addr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0084) + if err != nil { + t.Fatalf("unable to get current address: %v", addr) + } + p2wkhAddr, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Fatalf("unable to convert wallet address to p2wkh: %v", err) + } + + // Also create a nested P2WKH address we can use to send some coins to. + addr, err = w.CurrentAddress(0, waddrmgr.KeyScopeBIP0084) + if err != nil { + t.Fatalf("unable to get current address: %v", addr) + } + np2wkhAddr, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Fatalf("unable to convert wallet address to np2wkh: %v", err) + } + + // Register two big UTXO that will be used when funding the PSBT. + incomingTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{ + wire.NewTxOut(1000000, p2wkhAddr), + wire.NewTxOut(1000000, np2wkhAddr), + }, + } + addUtxo(t, w, incomingTx) + + testCases := []struct { + name string + packet *psbt.Packet + feeRateSatPerKB btcutil.Amount + expectedErr string + validatePackage bool + numExpectedInputs int + }{{ + name: "no outputs provided", + packet: &psbt.Packet{ + UnsignedTx: &wire.MsgTx{}, + }, + feeRateSatPerKB: 0, + expectedErr: "must contain at least one output", + }, { + name: "no dust outputs", + packet: &psbt.Packet{ + UnsignedTx: &wire.MsgTx{ + TxOut: []*wire.TxOut{{ + PkScript: []byte("foo"), + Value: 100, + }}, + }, + Outputs: []psbt.POutput{{}}, + }, + feeRateSatPerKB: 0, + expectedErr: "transaction output is dust", + }, { + name: "two outputs, no inputs", + packet: &psbt.Packet{ + UnsignedTx: &wire.MsgTx{ + TxOut: []*wire.TxOut{{ + PkScript: testScriptP2WSH, + Value: 100000, + }, { + PkScript: testScriptP2WKH, + Value: 50000, + }}, + }, + Outputs: []psbt.POutput{{}, {}}, + }, + feeRateSatPerKB: 2000, // 2 sat/byte + expectedErr: "", + validatePackage: true, + numExpectedInputs: 1, + }, { + name: "two outputs, two inputs", + packet: &psbt.Packet{ + UnsignedTx: &wire.MsgTx{ + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: incomingTx.TxHash(), + Index: 0, + }, + }, { + PreviousOutPoint: wire.OutPoint{ + Hash: incomingTx.TxHash(), + Index: 1, + }, + }}, + TxOut: []*wire.TxOut{{ + PkScript: testScriptP2WSH, + Value: 100000, + }, { + PkScript: testScriptP2WKH, + Value: 50000, + }}, + }, + Inputs: []psbt.PInput{{}, {}}, + Outputs: []psbt.POutput{{}, {}}, + }, + feeRateSatPerKB: 2000, // 2 sat/byte + expectedErr: "", + validatePackage: true, + numExpectedInputs: 2, + }} + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + err := w.FundPsbt(tc.packet, 0, tc.feeRateSatPerKB) + + // Make sure the error is what we expected. + if err == nil && tc.expectedErr != "" { + t.Fatalf("expected error '%s' but got nil", + tc.expectedErr) + } + if err != nil && tc.expectedErr == "" { + t.Fatalf("expected nil error but got '%v'", err) + } + if err != nil && + !strings.Contains(err.Error(), tc.expectedErr) { + + t.Fatalf("expected error '%s' but got '%v'", + tc.expectedErr, err) + } + + if !tc.validatePackage { + return + } + + // Check wire inputs. + packet := tc.packet + if len(packet.UnsignedTx.TxIn) != tc.numExpectedInputs { + t.Fatalf("expected %d inputs to be added, got "+ + "%d", tc.numExpectedInputs, + len(packet.UnsignedTx.TxIn)) + } + txIn := packet.UnsignedTx.TxIn[0] + if txIn.PreviousOutPoint.Hash != incomingTx.TxHash() { + t.Fatalf("unexpected UTXO prev outpoint "+ + "hash, got %v wanted %v", + txIn.PreviousOutPoint.Hash, + incomingTx.TxHash()) + } + if tc.numExpectedInputs > 1 { + txIn2 := packet.UnsignedTx.TxIn[1] + if txIn2.PreviousOutPoint.Hash != incomingTx.TxHash() { + t.Fatalf("unexpected UTXO prev outpoint "+ + "hash, got %v wanted %v", + txIn2.PreviousOutPoint.Hash, + incomingTx.TxHash()) + } + } + + // Check partial inputs. + if len(packet.Inputs) != tc.numExpectedInputs { + t.Fatalf("expected %d partial input to be "+ + "added, got %d", tc.numExpectedInputs, + len(packet.Inputs)) + } + in := packet.Inputs[0] + if in.WitnessUtxo == nil { + t.Fatalf("partial input witness UTXO not set") + } + if !bytes.Equal(in.WitnessUtxo.PkScript, p2wkhAddr) { + t.Fatalf("unexpected witness UTXO script, "+ + "got %x wanted %x", + in.WitnessUtxo.PkScript, p2wkhAddr) + } + if in.NonWitnessUtxo == nil { + t.Fatalf("partial input non-witness UTXO not " + + "set") + } + prevIdx := txIn.PreviousOutPoint.Index + nonWitnessOut := in.NonWitnessUtxo.TxOut[prevIdx] + if !bytes.Equal(nonWitnessOut.PkScript, p2wkhAddr) { + t.Fatalf("unexpected witness UTXO script, "+ + "got %x wanted %x", + nonWitnessOut.PkScript, p2wkhAddr) + } + if in.SighashType != txscript.SigHashAll { + t.Fatalf("unexpected sighash flag, got %d "+ + "wanted %d", in.SighashType, + txscript.SigHashAll) + } + if tc.numExpectedInputs > 1 { + in2 := packet.Inputs[1] + if in2.WitnessUtxo == nil { + t.Fatalf("partial input witness UTXO " + + "not set") + } + if !bytes.Equal(in2.WitnessUtxo.PkScript, np2wkhAddr) { + t.Fatalf("unexpected witness UTXO "+ + "script, got %x wanted %x", + in2.WitnessUtxo.PkScript, + np2wkhAddr) + } + if in2.NonWitnessUtxo == nil { + t.Fatalf("partial input non-witness " + + "UTXO not set") + } + txIn2 := packet.UnsignedTx.TxIn[1] + prevIdx2 := txIn2.PreviousOutPoint.Index + nonWitnessOut2 := in2.NonWitnessUtxo.TxOut[prevIdx2] + if !bytes.Equal(nonWitnessOut2.PkScript, p2wkhAddr) { + t.Fatalf("unexpected witness UTXO script, "+ + "got %x wanted %x", + nonWitnessOut2.PkScript, p2wkhAddr) + } + if in2.SighashType != txscript.SigHashAll { + t.Fatalf("unexpected sighash flag, "+ + "got %d wanted %d", + in2.SighashType, + txscript.SigHashAll) + } + } + + // Check outputs, find index for each of the 3 expected. + txOuts := packet.UnsignedTx.TxOut + if len(txOuts) != 3 { + t.Fatalf("unexpected outputs, got %d wanted 3", + len(txOuts)) + } + p2wkhIndex := -1 + p2wshIndex := -1 + changeIndex := -1 + for idx, txOut := range txOuts { + script := txOut.PkScript + + switch { + case bytes.Equal(script, testScriptP2WKH): + p2wkhIndex = idx + + case bytes.Equal(script, testScriptP2WSH): + p2wshIndex = idx + + default: + changeIndex = idx + } + } + + // All outputs must be found. + if p2wkhIndex < 0 || p2wshIndex < 0 || changeIndex < 0 { + t.Fatalf("not all outputs found, got indices "+ + "p2wkh=%d, p2wsh=%d, change=%d", + p2wkhIndex, p2wshIndex, changeIndex) + } + + // After BIP 69 sorting, the P2WKH output should be + // before the P2WSH output because the PK script is + // lexicographically smaller. + if p2wkhIndex > p2wshIndex { + t.Fatalf("expected output with script %x to "+ + "be before script %x", + txOuts[p2wkhIndex].PkScript, + txOuts[p2wshIndex].PkScript) + } + + // Finally, check the change output size and that it + // belongs to the wallet. + expectedFee := int64(368) + expectedChange := 1000000 - 150000 - expectedFee + if txOuts[changeIndex].Value != expectedChange { + t.Fatalf("unexpected change output size, got "+ + "%d wanted %d", + txOuts[changeIndex].Value, + expectedChange) + } + }) + } +} + +// TestFinalizePsbt tests that a given PSBT packet can be finalized. +func TestFinalizePsbt(t *testing.T) { + w, cleanup := testWallet(t) + defer cleanup() + + // Create a P2WKH address we can use to send some coins to. + addr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0084) + if err != nil { + t.Fatalf("unable to get current address: %v", addr) + } + p2wkhAddr, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Fatalf("unable to convert wallet address to p2wkh: %v", err) + } + + // Also create a nested P2WKH address we can send coins to. + addr, err = w.CurrentAddress(0, waddrmgr.KeyScopeBIP0049Plus) + if err != nil { + t.Fatalf("unable to get current address: %v", addr) + } + np2wkhAddr, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Fatalf("unable to convert wallet address to np2wkh: %v", err) + } + + // Register two big UTXO that will be used when funding the PSBT. + utxOutP2WKH := wire.NewTxOut(1000000, p2wkhAddr) + utxOutNP2WKH := wire.NewTxOut(1000000, np2wkhAddr) + incomingTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{utxOutP2WKH, utxOutNP2WKH}, + } + addUtxo(t, w, incomingTx) + + // Create the packet that we want to sign. + packet := &psbt.Packet{ + UnsignedTx: &wire.MsgTx{ + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: incomingTx.TxHash(), + Index: 0, + }, + }, { + PreviousOutPoint: wire.OutPoint{ + Hash: incomingTx.TxHash(), + Index: 1, + }, + }}, + TxOut: []*wire.TxOut{{ + PkScript: testScriptP2WKH, + Value: 50000, + }, { + PkScript: testScriptP2WSH, + Value: 100000, + }, { + PkScript: testScriptP2WKH, + Value: 849632, + }}, + }, + Inputs: []psbt.PInput{{ + WitnessUtxo: utxOutP2WKH, + SighashType: txscript.SigHashAll, + }, { + NonWitnessUtxo: incomingTx, + SighashType: txscript.SigHashAll, + }}, + Outputs: []psbt.POutput{{}, {}, {}}, + } + + // Finalize it to add all witness data then extract the final TX. + err = w.FinalizePsbt(packet) + if err != nil { + t.Fatalf("error finalizing PSBT packet: %v", err) + } + finalTx, err := psbt.Extract(packet) + if err != nil { + t.Fatalf("error extracting final TX from PSBT: %v", err) + } + + // Finally verify that the created witness is valid. + err = validateMsgTx( + finalTx, [][]byte{utxOutP2WKH.PkScript, utxOutNP2WKH.PkScript}, + []btcutil.Amount{1000000, 1000000}, + ) + if err != nil { + t.Fatalf("error validating tx: %v", err) + } +}