9f1eb98666
Setting the minimum confirmation is used for the coin selection when no inputs are provided.
427 lines
11 KiB
Go
427 lines
11 KiB
Go
// 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.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.
|
|
incomingTx1 := &wire.MsgTx{
|
|
TxIn: []*wire.TxIn{{}},
|
|
TxOut: []*wire.TxOut{wire.NewTxOut(1000000, p2wkhAddr)},
|
|
}
|
|
addUtxo(t, w, incomingTx1)
|
|
utxo1 := wire.OutPoint{
|
|
Hash: incomingTx1.TxHash(),
|
|
Index: 0,
|
|
}
|
|
|
|
incomingTx2 := &wire.MsgTx{
|
|
TxIn: []*wire.TxIn{{}},
|
|
TxOut: []*wire.TxOut{wire.NewTxOut(900000, np2wkhAddr)},
|
|
}
|
|
addUtxo(t, w, incomingTx2)
|
|
utxo2 := wire.OutPoint{
|
|
Hash: incomingTx2.TxHash(),
|
|
Index: 0,
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
packet *psbt.Packet
|
|
feeRateSatPerKB btcutil.Amount
|
|
expectedErr string
|
|
validatePackage bool
|
|
expectedFee int64
|
|
expectedChange int64
|
|
expectedInputs []wire.OutPoint
|
|
additionalChecks func(*testing.T, *psbt.Packet, int32)
|
|
}{{
|
|
name: "no outputs provided",
|
|
packet: &psbt.Packet{
|
|
UnsignedTx: &wire.MsgTx{},
|
|
},
|
|
feeRateSatPerKB: 0,
|
|
expectedErr: "PSBT packet must contain at least one input or output",
|
|
}, {
|
|
name: "single input, no outputs",
|
|
packet: &psbt.Packet{
|
|
UnsignedTx: &wire.MsgTx{
|
|
TxIn: []*wire.TxIn{{
|
|
PreviousOutPoint: utxo1,
|
|
}},
|
|
},
|
|
Inputs: []psbt.PInput{{}},
|
|
},
|
|
feeRateSatPerKB: 20000,
|
|
validatePackage: true,
|
|
expectedInputs: []wire.OutPoint{utxo1},
|
|
expectedFee: 2200,
|
|
expectedChange: 997800,
|
|
}, {
|
|
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,
|
|
expectedFee: 368,
|
|
expectedChange: 1000000 - 150000 - 368,
|
|
expectedInputs: []wire.OutPoint{utxo1},
|
|
}, {
|
|
name: "large output, no inputs",
|
|
packet: &psbt.Packet{
|
|
UnsignedTx: &wire.MsgTx{
|
|
TxOut: []*wire.TxOut{{
|
|
PkScript: testScriptP2WSH,
|
|
Value: 1500000,
|
|
}},
|
|
},
|
|
Outputs: []psbt.POutput{{}},
|
|
},
|
|
feeRateSatPerKB: 4000, // 4 sat/byte
|
|
expectedErr: "",
|
|
validatePackage: true,
|
|
expectedFee: 980,
|
|
expectedChange: 1900000 - 1500000 - 980,
|
|
expectedInputs: []wire.OutPoint{utxo1, utxo2},
|
|
}, {
|
|
name: "two outputs, two inputs",
|
|
packet: &psbt.Packet{
|
|
UnsignedTx: &wire.MsgTx{
|
|
TxIn: []*wire.TxIn{{
|
|
PreviousOutPoint: utxo1,
|
|
}, {
|
|
PreviousOutPoint: utxo2,
|
|
}},
|
|
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,
|
|
expectedFee: 552,
|
|
expectedChange: 1900000 - 150000 - 552,
|
|
expectedInputs: []wire.OutPoint{utxo1, utxo2},
|
|
additionalChecks: func(t *testing.T, packet *psbt.Packet,
|
|
changeIndex int32) {
|
|
|
|
// 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
|
|
totalOut := int64(0)
|
|
for idx, txOut := range txOuts {
|
|
script := txOut.PkScript
|
|
totalOut += txOut.Value
|
|
|
|
switch {
|
|
case bytes.Equal(script, testScriptP2WKH):
|
|
p2wkhIndex = idx
|
|
|
|
case bytes.Equal(script, testScriptP2WSH):
|
|
p2wshIndex = idx
|
|
|
|
}
|
|
}
|
|
totalIn := int64(0)
|
|
for _, txIn := range packet.Inputs {
|
|
totalIn += txIn.WitnessUtxo.Value
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
},
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
changeIndex, err := w.FundPsbt(
|
|
tc.packet, nil, 1, 0, tc.feeRateSatPerKB,
|
|
CoinSelectionLargest,
|
|
)
|
|
|
|
// In any case, unlock the UTXO before continuing, we
|
|
// don't want to pollute other test iterations.
|
|
for _, in := range tc.packet.UnsignedTx.TxIn {
|
|
w.UnlockOutpoint(in.PreviousOutPoint)
|
|
}
|
|
|
|
// 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
|
|
assertTxInputs(t, packet, tc.expectedInputs)
|
|
|
|
// Run any additional tests if available.
|
|
if tc.additionalChecks != nil {
|
|
tc.additionalChecks(t, packet, changeIndex)
|
|
}
|
|
|
|
// Finally, check the change output size and fee.
|
|
txOuts := packet.UnsignedTx.TxOut
|
|
totalOut := int64(0)
|
|
for _, txOut := range txOuts {
|
|
totalOut += txOut.Value
|
|
}
|
|
totalIn := int64(0)
|
|
for _, txIn := range packet.Inputs {
|
|
totalIn += txIn.WitnessUtxo.Value
|
|
}
|
|
fee := totalIn - totalOut
|
|
if fee != tc.expectedFee {
|
|
t.Fatalf("unexpected fee, got %d wanted %d",
|
|
fee, tc.expectedFee)
|
|
}
|
|
if txOuts[changeIndex].Value != tc.expectedChange {
|
|
t.Fatalf("unexpected change output size, got "+
|
|
"%d wanted %d",
|
|
txOuts[changeIndex].Value,
|
|
tc.expectedChange)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertTxInputs(t *testing.T, packet *psbt.Packet,
|
|
expected []wire.OutPoint) {
|
|
|
|
if len(packet.UnsignedTx.TxIn) != len(expected) {
|
|
t.Fatalf("expected %d inputs to be added, got %d",
|
|
len(expected), len(packet.UnsignedTx.TxIn))
|
|
}
|
|
|
|
// The order of the UTXOs is random, we need to loop through each of
|
|
// them to make sure they're found. We also check that no signature data
|
|
// was added yet.
|
|
for _, txIn := range packet.UnsignedTx.TxIn {
|
|
if !containsUtxo(expected, txIn.PreviousOutPoint) {
|
|
t.Fatalf("outpoint %v not found in list of expected "+
|
|
"UTXOs", txIn.PreviousOutPoint)
|
|
}
|
|
|
|
if len(txIn.SignatureScript) > 0 {
|
|
t.Fatalf("expected scriptSig to be empty on "+
|
|
"txin, got %x instead",
|
|
txIn.SignatureScript)
|
|
}
|
|
if len(txIn.Witness) > 0 {
|
|
t.Fatalf("expected witness to be empty on "+
|
|
"txin, got %v instead",
|
|
txIn.Witness)
|
|
}
|
|
}
|
|
}
|
|
|
|
func containsUtxo(list []wire.OutPoint, candidate wire.OutPoint) bool {
|
|
for _, utxo := range list {
|
|
if utxo == candidate {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// 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(nil, 0, 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)
|
|
}
|
|
}
|