Merge pull request #711 from guggero/psbt-signing
wallet: add new PSBT funding and finalizing methods
This commit is contained in:
commit
2c5947a452
10 changed files with 1131 additions and 58 deletions
1
go.mod
1
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
305
wallet/psbt.go
Normal file
305
wallet/psbt.go
Normal file
|
@ -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
|
||||
}
|
395
wallet/psbt_test.go
Normal file
395
wallet/psbt_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
102
wallet/signer.go
Normal file
102
wallet/signer.go
Normal file
|
@ -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
|
||||
}
|
103
wallet/signer_test.go
Normal file
103
wallet/signer_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
61
wallet/utxos_test.go
Normal file
61
wallet/utxos_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue