mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-26 08:55:59 +01:00
walletrpc: implement third PSBT funding option
And now we finally implement the new funding option: Allowing coin selection with pre-defined inputs and outputs.
This commit is contained in:
parent
0f6c25a773
commit
094fdbfa72
2 changed files with 954 additions and 3 deletions
|
@ -38,6 +38,7 @@ import (
|
|||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/lightningnetwork/lnd/sweep"
|
||||
"google.golang.org/grpc"
|
||||
|
@ -1234,10 +1235,11 @@ func (w *WalletKit) FundPsbt(_ context.Context,
|
|||
account = req.Account
|
||||
}
|
||||
|
||||
// There are two ways a user can specify what we call the template (a
|
||||
// There are three ways a user can specify what we call the template (a
|
||||
// list of inputs and outputs to use in the PSBT): Either as a PSBT
|
||||
// packet directly or as a special RPC message. Find out which one the
|
||||
// user wants to use, they are mutually exclusive.
|
||||
// packet directly with no coin selection, a PSBT with coin selection or
|
||||
// as a special RPC message. Find out which one the user wants to use,
|
||||
// they are mutually exclusive.
|
||||
switch {
|
||||
// The template is specified as a PSBT. All we have to do is parse it.
|
||||
case req.GetPsbt() != nil:
|
||||
|
@ -1254,6 +1256,82 @@ func (w *WalletKit) FundPsbt(_ context.Context,
|
|||
packet, minConfs, feeSatPerKW,
|
||||
)
|
||||
|
||||
// The template is specified as a PSBT with the intention to perform
|
||||
// coin selection even if inputs are already present.
|
||||
case req.GetCoinSelect() != nil:
|
||||
coinSelectRequest := req.GetCoinSelect()
|
||||
r := bytes.NewReader(coinSelectRequest.Psbt)
|
||||
packet, err := psbt.NewFromRawBytes(r, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse PSBT: %w", err)
|
||||
}
|
||||
|
||||
numOutputs := int32(len(packet.UnsignedTx.TxOut))
|
||||
if numOutputs == 0 {
|
||||
return nil, fmt.Errorf("no outputs specified in " +
|
||||
"template")
|
||||
}
|
||||
|
||||
outputSum := int64(0)
|
||||
for _, txOut := range packet.UnsignedTx.TxOut {
|
||||
outputSum += txOut.Value
|
||||
}
|
||||
if outputSum <= 0 {
|
||||
return nil, fmt.Errorf("output sum must be positive")
|
||||
}
|
||||
|
||||
var (
|
||||
changeIndex int32 = -1
|
||||
changeType chanfunding.ChangeAddressType
|
||||
)
|
||||
switch t := coinSelectRequest.ChangeOutput.(type) {
|
||||
// The user wants to use an existing output as change output.
|
||||
case *PsbtCoinSelect_ExistingOutputIndex:
|
||||
if t.ExistingOutputIndex < 0 ||
|
||||
t.ExistingOutputIndex >= numOutputs {
|
||||
|
||||
return nil, fmt.Errorf("change output index "+
|
||||
"out of range: %d",
|
||||
t.ExistingOutputIndex)
|
||||
}
|
||||
|
||||
changeIndex = t.ExistingOutputIndex
|
||||
|
||||
changeOut := packet.UnsignedTx.TxOut[changeIndex]
|
||||
_, err := txscript.ParsePkScript(changeOut.PkScript)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing change "+
|
||||
"script: %w", err)
|
||||
}
|
||||
|
||||
changeType = chanfunding.ExistingChangeAddress
|
||||
|
||||
// The user wants to use a new output as change output.
|
||||
case *PsbtCoinSelect_Add:
|
||||
// We already set the change index to -1 above to
|
||||
// indicate no change output should be used if possible
|
||||
// or a new one should be created if needed. So we only
|
||||
// need to parse the type of change output we want to
|
||||
// create.
|
||||
switch req.ChangeType {
|
||||
case ChangeAddressType_CHANGE_ADDRESS_TYPE_P2TR:
|
||||
changeType = chanfunding.P2TRChangeAddress
|
||||
|
||||
default:
|
||||
changeType = chanfunding.P2WKHChangeAddress
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown change output type")
|
||||
}
|
||||
|
||||
// Run the actual funding process now, using the channel funding
|
||||
// coin selection algorithm.
|
||||
return w.fundPsbtCoinSelect(
|
||||
account, changeIndex, packet, minConfs, changeType,
|
||||
feeSatPerKW,
|
||||
)
|
||||
|
||||
// The template is specified as a RPC message. We need to create a new
|
||||
// PSBT and copy the RPC information over.
|
||||
case req.GetRaw() != nil:
|
||||
|
@ -1385,6 +1463,242 @@ func (w *WalletKit) fundPsbtInternalWallet(account string,
|
|||
return response, nil
|
||||
}
|
||||
|
||||
// fundPsbtCoinSelect uses the "new" PSBT funding method using the channel
|
||||
// funding coin selection algorithm that allows specifying custom inputs while
|
||||
// selecting coins.
|
||||
func (w *WalletKit) fundPsbtCoinSelect(account string, changeIndex int32,
|
||||
packet *psbt.Packet, minConfs int32,
|
||||
changeType chanfunding.ChangeAddressType,
|
||||
feeRate chainfee.SatPerKWeight) (*FundPsbtResponse, error) {
|
||||
|
||||
// We want to make sure we don't select any inputs that are already
|
||||
// specified in the template. To do that, we require those inputs to
|
||||
// either not belong to this lnd at all or to be already locked through
|
||||
// a manual lock call by the user. Either way, they should not appear in
|
||||
// the list of unspent outputs.
|
||||
err := w.assertNotAvailable(packet.UnsignedTx.TxIn, minConfs, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// In case the user just specified the input outpoints of UTXOs we own,
|
||||
// the fee estimation below will error out because the UTXO information
|
||||
// is missing. We need to fetch the UTXO information from the wallet
|
||||
// and add it to the PSBT. We ignore inputs we don't actually know as
|
||||
// they could belong to another wallet.
|
||||
err = w.cfg.Wallet.DecorateInputs(packet, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decorating inputs: %w", err)
|
||||
}
|
||||
|
||||
// Before we select anything, we need to calculate the input, output and
|
||||
// current weight amounts. While doing that we also ensure the PSBT has
|
||||
// all the required information we require at this step.
|
||||
var (
|
||||
inputSum, outputSum btcutil.Amount
|
||||
estimator input.TxWeightEstimator
|
||||
)
|
||||
for i := range packet.Inputs {
|
||||
in := packet.Inputs[i]
|
||||
|
||||
err := btcwallet.EstimateInputWeight(&in, &estimator)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error estimating input "+
|
||||
"weight: %w", err)
|
||||
}
|
||||
|
||||
inputSum += btcutil.Amount(in.WitnessUtxo.Value)
|
||||
}
|
||||
for i := range packet.UnsignedTx.TxOut {
|
||||
out := packet.UnsignedTx.TxOut[i]
|
||||
|
||||
estimator.AddOutput(out.PkScript)
|
||||
outputSum += btcutil.Amount(out.Value)
|
||||
}
|
||||
|
||||
// The amount we want to fund is the total output sum plus the current
|
||||
// fee estimate, minus the sum of any already specified inputs. Since we
|
||||
// pass the estimator of the current transaction into the coin selection
|
||||
// algorithm, we don't need to subtract the fees here.
|
||||
fundingAmount := outputSum - inputSum
|
||||
|
||||
var changeDustLimit btcutil.Amount
|
||||
switch changeType {
|
||||
case chanfunding.P2TRChangeAddress:
|
||||
changeDustLimit = lnwallet.DustLimitForSize(input.P2TRSize)
|
||||
|
||||
case chanfunding.P2WKHChangeAddress:
|
||||
changeDustLimit = lnwallet.DustLimitForSize(input.P2WPKHSize)
|
||||
|
||||
case chanfunding.ExistingChangeAddress:
|
||||
changeOut := packet.UnsignedTx.TxOut[changeIndex]
|
||||
changeDustLimit = lnwallet.DustLimitForSize(
|
||||
len(changeOut.PkScript),
|
||||
)
|
||||
}
|
||||
|
||||
// Do we already have enough inputs specified to pay for the TX as it
|
||||
// is? In that case we only need to allocate any change, if there is
|
||||
// any.
|
||||
packetFeeNoChange := feeRate.FeeForWeight(int64(estimator.Weight()))
|
||||
if inputSum >= outputSum+packetFeeNoChange {
|
||||
// Calculate the packet's fee with a change output so, so we can
|
||||
// let the coin selection algorithm decide whether to use a
|
||||
// change output or not.
|
||||
switch changeType {
|
||||
case chanfunding.P2TRChangeAddress:
|
||||
estimator.AddP2TROutput()
|
||||
|
||||
case chanfunding.P2WKHChangeAddress:
|
||||
estimator.AddP2WKHOutput()
|
||||
}
|
||||
packetFeeWithChange := feeRate.FeeForWeight(
|
||||
int64(estimator.Weight()),
|
||||
)
|
||||
|
||||
changeAmt, needMore, err := chanfunding.CalculateChangeAmount(
|
||||
inputSum, outputSum, packetFeeNoChange,
|
||||
packetFeeWithChange, changeDustLimit, changeType,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error calculating change "+
|
||||
"amount: %w", err)
|
||||
}
|
||||
|
||||
// We shouldn't get into this branch if the input sum isn't
|
||||
// enough to pay for the current package without a change
|
||||
// output. So this should never be non-zero.
|
||||
if needMore != 0 {
|
||||
return nil, fmt.Errorf("internal error with change " +
|
||||
"amount calculation")
|
||||
}
|
||||
|
||||
if changeAmt > 0 {
|
||||
changeIndex, err = w.handleChange(
|
||||
packet, changeIndex, int64(changeAmt),
|
||||
changeType, account,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error handling change "+
|
||||
"amount: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// We're done. Let's serialize and return the updated package.
|
||||
return w.lockAndCreateFundingResponse(packet, nil, changeIndex)
|
||||
}
|
||||
|
||||
// The RPC parsing part is now over. Several of the following operations
|
||||
// require us to hold the global coin selection lock, so we do the rest
|
||||
// of the tasks while holding the lock. The result is a list of locked
|
||||
// UTXOs.
|
||||
var response *FundPsbtResponse
|
||||
err = w.cfg.CoinSelectionLocker.WithCoinSelectLock(func() error {
|
||||
// Get a list of all unspent witness outputs.
|
||||
utxos, err := w.cfg.Wallet.ListUnspentWitness(
|
||||
minConfs, defaultMaxConf, account,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
coins := make([]base.Coin, len(utxos))
|
||||
for i, utxo := range utxos {
|
||||
coins[i] = base.Coin{
|
||||
TxOut: wire.TxOut{
|
||||
Value: int64(utxo.Value),
|
||||
PkScript: utxo.PkScript,
|
||||
},
|
||||
OutPoint: utxo.OutPoint,
|
||||
}
|
||||
}
|
||||
|
||||
selectedCoins, changeAmount, err := chanfunding.CoinSelect(
|
||||
feeRate, fundingAmount, changeDustLimit, coins,
|
||||
w.cfg.CoinSelectionStrategy, estimator, changeType,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error selecting coins: %w", err)
|
||||
}
|
||||
|
||||
if changeAmount > 0 {
|
||||
changeIndex, err = w.handleChange(
|
||||
packet, changeIndex, int64(changeAmount),
|
||||
changeType, account,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error handling change "+
|
||||
"amount: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
addedOutpoints := make([]wire.OutPoint, len(selectedCoins))
|
||||
for i := range selectedCoins {
|
||||
coin := selectedCoins[i]
|
||||
addedOutpoints[i] = coin.OutPoint
|
||||
|
||||
packet.UnsignedTx.TxIn = append(
|
||||
packet.UnsignedTx.TxIn, &wire.TxIn{
|
||||
PreviousOutPoint: coin.OutPoint,
|
||||
},
|
||||
)
|
||||
packet.Inputs = append(packet.Inputs, psbt.PInput{
|
||||
WitnessUtxo: &coin.TxOut,
|
||||
})
|
||||
}
|
||||
|
||||
// Now that we've added the bare TX inputs, we also need to add
|
||||
// the more verbose input information to the packet, so a future
|
||||
// signer doesn't need to do any lookups. We skip any inputs
|
||||
// that our wallet doesn't own.
|
||||
err = w.cfg.Wallet.DecorateInputs(packet, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decorating inputs: %w", err)
|
||||
}
|
||||
|
||||
response, err = w.lockAndCreateFundingResponse(
|
||||
packet, addedOutpoints, changeIndex,
|
||||
)
|
||||
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// assertNotAvailable makes sure the specified inputs either don't belong to
|
||||
// this node or are already locked by the user.
|
||||
func (w *WalletKit) assertNotAvailable(inputs []*wire.TxIn, minConfs int32,
|
||||
account string) error {
|
||||
|
||||
return w.cfg.CoinSelectionLocker.WithCoinSelectLock(func() error {
|
||||
// Get a list of all unspent witness outputs.
|
||||
utxos, err := w.cfg.Wallet.ListUnspentWitness(
|
||||
minConfs, defaultMaxConf, account,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching UTXOs: %w", err)
|
||||
}
|
||||
|
||||
// We'll now check that none of the inputs specified in the
|
||||
// template are available to us. That means they either don't
|
||||
// belong to us or are already locked by the user.
|
||||
for _, txIn := range inputs {
|
||||
for _, utxo := range utxos {
|
||||
if txIn.PreviousOutPoint == utxo.OutPoint {
|
||||
return fmt.Errorf("input %v is not "+
|
||||
"locked", txIn.PreviousOutPoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// lockAndCreateFundingResponse locks the given outpoints and creates a funding
|
||||
// response with the serialized PSBT, the change index and the locked UTXOs.
|
||||
func (w *WalletKit) lockAndCreateFundingResponse(packet *psbt.Packet,
|
||||
|
@ -1415,6 +1729,47 @@ func (w *WalletKit) lockAndCreateFundingResponse(packet *psbt.Packet,
|
|||
}, nil
|
||||
}
|
||||
|
||||
// handleChange is a closure that either adds the non-zero change amount to an
|
||||
// existing output or creates a change output. The function returns the new
|
||||
// change output index if a new change output was added.
|
||||
func (w *WalletKit) handleChange(packet *psbt.Packet, changeIndex int32,
|
||||
changeAmount int64, changeType chanfunding.ChangeAddressType,
|
||||
changeAccount string) (int32, error) {
|
||||
|
||||
// Does an existing output get the change?
|
||||
if changeIndex >= 0 {
|
||||
changeOut := packet.UnsignedTx.TxOut[changeIndex]
|
||||
changeOut.Value += changeAmount
|
||||
|
||||
return changeIndex, nil
|
||||
}
|
||||
|
||||
// The user requested a new change output.
|
||||
addrType := addrTypeFromChangeAddressType(changeType)
|
||||
changeAddr, err := w.cfg.Wallet.NewAddress(
|
||||
addrType, true, changeAccount,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("could not derive change address: %w", err)
|
||||
}
|
||||
|
||||
changeScript, err := txscript.PayToAddrScript(changeAddr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("could not derive change script: %w", err)
|
||||
}
|
||||
|
||||
newChangeIndex := int32(len(packet.Outputs))
|
||||
packet.UnsignedTx.TxOut = append(
|
||||
packet.UnsignedTx.TxOut, &wire.TxOut{
|
||||
Value: changeAmount,
|
||||
PkScript: changeScript,
|
||||
},
|
||||
)
|
||||
packet.Outputs = append(packet.Outputs, psbt.POutput{})
|
||||
|
||||
return newChangeIndex, nil
|
||||
}
|
||||
|
||||
// marshallLeases converts the lock leases to the RPC format.
|
||||
func marshallLeases(locks []*base.ListLeasedOutputResult) []*UtxoLease {
|
||||
rpcLocks := make([]*UtxoLease, len(locks))
|
||||
|
@ -1448,6 +1803,20 @@ func keyScopeFromChangeAddressType(
|
|||
}
|
||||
}
|
||||
|
||||
// addrTypeFromChangeAddressType maps a chanfunding.ChangeAddressType to the
|
||||
// lnwallet.AddressType.
|
||||
func addrTypeFromChangeAddressType(
|
||||
changeAddressType chanfunding.ChangeAddressType) lnwallet.AddressType {
|
||||
|
||||
switch changeAddressType {
|
||||
case chanfunding.P2TRChangeAddress:
|
||||
return lnwallet.TaprootPubkey
|
||||
|
||||
default:
|
||||
return lnwallet.WitnessPubKey
|
||||
}
|
||||
}
|
||||
|
||||
// SignPsbt expects a partial transaction with all inputs and outputs fully
|
||||
// declared and tries to sign all unsigned inputs that have all required fields
|
||||
// (UTXO information, BIP32 derivation information, witness or sig scripts)
|
||||
|
|
|
@ -4,9 +4,24 @@
|
|||
package walletrpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcwallet/wallet"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntest/mock"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -48,3 +63,570 @@ func TestWitnessTypeMapping(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockCoinSelectionLocker struct {
|
||||
fail bool
|
||||
}
|
||||
|
||||
func (m *mockCoinSelectionLocker) WithCoinSelectLock(f func() error) error {
|
||||
if err := f(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m.fail {
|
||||
return fmt.Errorf("kek")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestFundPsbtCoinSelect tests that the coin selection for a PSBT template
|
||||
// works as expected.
|
||||
func TestFundPsbtCoinSelect(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const fundAmt = 50_000
|
||||
var (
|
||||
p2wkhDustLimit = lnwallet.DustLimitForSize(input.P2WPKHSize)
|
||||
p2trDustLimit = lnwallet.DustLimitForSize(input.P2TRSize)
|
||||
p2wkhScript, _ = input.WitnessPubKeyHash([]byte{})
|
||||
p2trScript, _ = txscript.PayToTaprootScript(
|
||||
&input.TaprootNUMSKey,
|
||||
)
|
||||
)
|
||||
|
||||
makePacket := func(outs ...*wire.TxOut) *psbt.Packet {
|
||||
p := &psbt.Packet{
|
||||
UnsignedTx: &wire.MsgTx{},
|
||||
}
|
||||
|
||||
for _, out := range outs {
|
||||
p.UnsignedTx.TxOut = append(p.UnsignedTx.TxOut, out)
|
||||
p.Outputs = append(p.Outputs, psbt.POutput{})
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
updatePacket := func(p *psbt.Packet,
|
||||
f func(*psbt.Packet) *psbt.Packet) *psbt.Packet {
|
||||
|
||||
return f(p)
|
||||
}
|
||||
calcFee := func(p2trIn, p2wkhIn, p2trOut, p2wkhOut int,
|
||||
dust btcutil.Amount) btcutil.Amount {
|
||||
|
||||
estimator := input.TxWeightEstimator{}
|
||||
for i := 0; i < p2trIn; i++ {
|
||||
estimator.AddTaprootKeySpendInput(
|
||||
txscript.SigHashDefault,
|
||||
)
|
||||
}
|
||||
for i := 0; i < p2wkhIn; i++ {
|
||||
estimator.AddP2WKHInput()
|
||||
}
|
||||
for i := 0; i < p2trOut; i++ {
|
||||
estimator.AddP2TROutput()
|
||||
}
|
||||
for i := 0; i < p2wkhOut; i++ {
|
||||
estimator.AddP2WKHOutput()
|
||||
}
|
||||
|
||||
weight := estimator.Weight()
|
||||
fee := chainfee.FeePerKwFloor.FeeForWeight(int64(weight))
|
||||
|
||||
return fee + dust
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
utxos []*lnwallet.Utxo
|
||||
packet *psbt.Packet
|
||||
changeIndex int32
|
||||
changeType chanfunding.ChangeAddressType
|
||||
feeRate chainfee.SatPerKWeight
|
||||
|
||||
// expectedUtxoIndexes is the list of utxo indexes that are
|
||||
// expected to be used for funding the psbt.
|
||||
expectedUtxoIndexes []int
|
||||
|
||||
// expectChangeOutputIndex is the expected output index that is
|
||||
// returned from the tested method.
|
||||
expectChangeOutputIndex int32
|
||||
|
||||
// expectedChangeOutputAmount is the expected final total amount
|
||||
// of the output marked as the change output. This will only be
|
||||
// checked if the expected amount is non-zero.
|
||||
expectedChangeOutputAmount btcutil.Amount
|
||||
|
||||
// expectedFee is the total amount of fees paid by the funded
|
||||
// packet in bytes.
|
||||
expectedFee btcutil.Amount
|
||||
|
||||
// expectedErr is the expected concrete error. If not nil, then
|
||||
// the error must match exactly.
|
||||
expectedErr error
|
||||
|
||||
// expectedErrType is the expected error type. If not nil, then
|
||||
// the error must be of this type.
|
||||
expectedErrType error
|
||||
}{{
|
||||
name: "no utxos",
|
||||
utxos: []*lnwallet.Utxo{},
|
||||
packet: makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2trScript,
|
||||
}),
|
||||
changeIndex: -1,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
expectedErrType: &chanfunding.ErrInsufficientFunds{},
|
||||
}, {
|
||||
name: "1 p2wpkh utxo, add p2wkh change",
|
||||
utxos: []*lnwallet.Utxo{
|
||||
{
|
||||
Value: 100_000,
|
||||
PkScript: p2wkhScript,
|
||||
},
|
||||
},
|
||||
packet: makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2trScript,
|
||||
}),
|
||||
changeIndex: -1,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
expectedUtxoIndexes: []int{0},
|
||||
expectChangeOutputIndex: 1,
|
||||
expectedFee: calcFee(0, 1, 1, 1, 0),
|
||||
}, {
|
||||
name: "1 p2wpkh utxo, add p2tr change",
|
||||
utxos: []*lnwallet.Utxo{
|
||||
{
|
||||
Value: 100_000,
|
||||
PkScript: p2wkhScript,
|
||||
},
|
||||
},
|
||||
packet: makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2trScript,
|
||||
}),
|
||||
changeIndex: -1,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
changeType: chanfunding.P2TRChangeAddress,
|
||||
expectedUtxoIndexes: []int{0},
|
||||
expectChangeOutputIndex: 1,
|
||||
expectedFee: calcFee(0, 1, 2, 0, 0),
|
||||
}, {
|
||||
name: "1 p2wpkh utxo, no change, exact amount",
|
||||
utxos: []*lnwallet.Utxo{
|
||||
{
|
||||
Value: fundAmt + 123,
|
||||
PkScript: p2wkhScript,
|
||||
},
|
||||
},
|
||||
packet: makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2trScript,
|
||||
}),
|
||||
changeIndex: -1,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
expectedUtxoIndexes: []int{0},
|
||||
expectChangeOutputIndex: -1,
|
||||
expectedFee: calcFee(0, 1, 1, 0, 0),
|
||||
}, {
|
||||
name: "1 p2wpkh utxo, no change, p2wpkh change dust to fee",
|
||||
utxos: []*lnwallet.Utxo{
|
||||
{
|
||||
Value: fundAmt + calcFee(
|
||||
0, 1, 1, 0, p2wkhDustLimit-1,
|
||||
),
|
||||
PkScript: p2wkhScript,
|
||||
},
|
||||
},
|
||||
packet: makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2trScript,
|
||||
}),
|
||||
changeIndex: -1,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
changeType: chanfunding.P2WKHChangeAddress,
|
||||
expectedUtxoIndexes: []int{0},
|
||||
expectChangeOutputIndex: -1,
|
||||
expectedFee: calcFee(0, 1, 1, 0, p2wkhDustLimit-1),
|
||||
}, {
|
||||
name: "1 p2wpkh utxo, no change, p2tr change dust to fee",
|
||||
utxos: []*lnwallet.Utxo{
|
||||
{
|
||||
Value: fundAmt + calcFee(
|
||||
0, 1, 1, 0, p2trDustLimit-1,
|
||||
),
|
||||
PkScript: p2wkhScript,
|
||||
},
|
||||
},
|
||||
packet: makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2trScript,
|
||||
}),
|
||||
changeIndex: -1,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
changeType: chanfunding.P2TRChangeAddress,
|
||||
expectedUtxoIndexes: []int{0},
|
||||
expectChangeOutputIndex: -1,
|
||||
expectedFee: calcFee(0, 1, 1, 0, p2trDustLimit-1),
|
||||
}, {
|
||||
name: "1 p2wpkh utxo, existing p2tr change",
|
||||
utxos: []*lnwallet.Utxo{
|
||||
{
|
||||
Value: fundAmt + 50_000,
|
||||
PkScript: p2wkhScript,
|
||||
},
|
||||
},
|
||||
packet: makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2trScript,
|
||||
}),
|
||||
changeIndex: 0,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
changeType: chanfunding.ExistingChangeAddress,
|
||||
expectedUtxoIndexes: []int{0},
|
||||
expectChangeOutputIndex: 0,
|
||||
expectedFee: calcFee(0, 1, 1, 0, 0),
|
||||
}, {
|
||||
name: "1 p2wpkh utxo, existing p2wkh change",
|
||||
utxos: []*lnwallet.Utxo{
|
||||
{
|
||||
Value: fundAmt + 50_000,
|
||||
PkScript: p2wkhScript,
|
||||
},
|
||||
},
|
||||
packet: makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2wkhScript,
|
||||
}),
|
||||
changeIndex: 0,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
changeType: chanfunding.ExistingChangeAddress,
|
||||
expectedUtxoIndexes: []int{0},
|
||||
expectChangeOutputIndex: 0,
|
||||
expectedFee: calcFee(0, 1, 0, 1, 0),
|
||||
}, {
|
||||
name: "1 p2wpkh utxo, existing p2wkh change, dust change",
|
||||
utxos: []*lnwallet.Utxo{
|
||||
{
|
||||
Value: fundAmt + calcFee(0, 1, 0, 1, 0) + 50,
|
||||
PkScript: p2wkhScript,
|
||||
},
|
||||
},
|
||||
packet: makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2wkhScript,
|
||||
}),
|
||||
changeIndex: 0,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
changeType: chanfunding.ExistingChangeAddress,
|
||||
expectedUtxoIndexes: []int{0},
|
||||
expectChangeOutputIndex: 0,
|
||||
expectedFee: calcFee(0, 1, 0, 1, 0),
|
||||
}, {
|
||||
name: "1 p2wpkh + 1 p2tr utxo, existing p2tr input, existing " +
|
||||
"p2tr change",
|
||||
utxos: []*lnwallet.Utxo{
|
||||
{
|
||||
Value: fundAmt / 2,
|
||||
PkScript: p2wkhScript,
|
||||
}, {
|
||||
Value: fundAmt / 2,
|
||||
PkScript: p2trScript,
|
||||
},
|
||||
},
|
||||
packet: updatePacket(makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2trScript,
|
||||
}), func(p *psbt.Packet) *psbt.Packet {
|
||||
p.UnsignedTx.TxIn = append(
|
||||
p.UnsignedTx.TxIn, &wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash{1, 2, 3},
|
||||
},
|
||||
},
|
||||
)
|
||||
p2TrDerivations := []*psbt.TaprootBip32Derivation{
|
||||
{
|
||||
XOnlyPubKey: schnorr.SerializePubKey(
|
||||
&input.TaprootNUMSKey,
|
||||
),
|
||||
Bip32Path: []uint32{1, 2, 3},
|
||||
},
|
||||
}
|
||||
p.Inputs = append(p.Inputs, psbt.PInput{
|
||||
WitnessUtxo: &wire.TxOut{
|
||||
Value: 1000,
|
||||
PkScript: p2trScript,
|
||||
},
|
||||
SighashType: txscript.SigHashSingle,
|
||||
TaprootBip32Derivation: p2TrDerivations,
|
||||
})
|
||||
|
||||
return p
|
||||
}),
|
||||
changeIndex: 0,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
changeType: chanfunding.ExistingChangeAddress,
|
||||
expectedUtxoIndexes: []int{0, 1},
|
||||
expectChangeOutputIndex: 0,
|
||||
expectedFee: calcFee(2, 1, 1, 0, 0),
|
||||
}, {
|
||||
name: "1 p2wpkh + 1 p2tr utxo, existing p2tr input, add p2tr " +
|
||||
"change",
|
||||
utxos: []*lnwallet.Utxo{
|
||||
{
|
||||
Value: fundAmt / 2,
|
||||
PkScript: p2wkhScript,
|
||||
}, {
|
||||
Value: fundAmt / 2,
|
||||
PkScript: p2trScript,
|
||||
},
|
||||
},
|
||||
packet: updatePacket(makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2trScript,
|
||||
}), func(p *psbt.Packet) *psbt.Packet {
|
||||
p.UnsignedTx.TxIn = append(
|
||||
p.UnsignedTx.TxIn, &wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash{1, 2, 3},
|
||||
},
|
||||
},
|
||||
)
|
||||
p2TrDerivations := []*psbt.TaprootBip32Derivation{
|
||||
{
|
||||
XOnlyPubKey: schnorr.SerializePubKey(
|
||||
&input.TaprootNUMSKey,
|
||||
),
|
||||
Bip32Path: []uint32{1, 2, 3},
|
||||
},
|
||||
}
|
||||
p.Inputs = append(p.Inputs, psbt.PInput{
|
||||
WitnessUtxo: &wire.TxOut{
|
||||
Value: 1000,
|
||||
PkScript: p2trScript,
|
||||
},
|
||||
SighashType: txscript.SigHashSingle,
|
||||
TaprootBip32Derivation: p2TrDerivations,
|
||||
})
|
||||
|
||||
return p
|
||||
}),
|
||||
changeIndex: -1,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
changeType: chanfunding.P2TRChangeAddress,
|
||||
expectedUtxoIndexes: []int{0, 1},
|
||||
expectChangeOutputIndex: 1,
|
||||
expectedFee: calcFee(2, 1, 2, 0, 0),
|
||||
}, {
|
||||
name: "large existing p2tr input, fee estimation p2wpkh " +
|
||||
"change",
|
||||
utxos: []*lnwallet.Utxo{},
|
||||
packet: updatePacket(makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2trScript,
|
||||
}), func(p *psbt.Packet) *psbt.Packet {
|
||||
p.UnsignedTx.TxIn = append(
|
||||
p.UnsignedTx.TxIn, &wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash{1, 2, 3},
|
||||
},
|
||||
},
|
||||
)
|
||||
p2TrDerivations := []*psbt.TaprootBip32Derivation{
|
||||
{
|
||||
XOnlyPubKey: schnorr.SerializePubKey(
|
||||
&input.TaprootNUMSKey,
|
||||
),
|
||||
Bip32Path: []uint32{1, 2, 3},
|
||||
},
|
||||
}
|
||||
p.Inputs = append(p.Inputs, psbt.PInput{
|
||||
WitnessUtxo: &wire.TxOut{
|
||||
Value: fundAmt * 3,
|
||||
PkScript: p2trScript,
|
||||
},
|
||||
TaprootBip32Derivation: p2TrDerivations,
|
||||
})
|
||||
|
||||
return p
|
||||
}),
|
||||
changeIndex: -1,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
changeType: chanfunding.P2WKHChangeAddress,
|
||||
expectedUtxoIndexes: []int{},
|
||||
expectChangeOutputIndex: 1,
|
||||
expectedChangeOutputAmount: fundAmt*3 - fundAmt -
|
||||
calcFee(1, 0, 1, 1, 0),
|
||||
expectedFee: calcFee(1, 0, 1, 1, 0),
|
||||
}, {
|
||||
name: "large existing p2tr input, fee estimation no change",
|
||||
utxos: []*lnwallet.Utxo{},
|
||||
packet: updatePacket(makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2trScript,
|
||||
}), func(p *psbt.Packet) *psbt.Packet {
|
||||
p.UnsignedTx.TxIn = append(
|
||||
p.UnsignedTx.TxIn, &wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash{1, 2, 3},
|
||||
},
|
||||
},
|
||||
)
|
||||
p2TrDerivations := []*psbt.TaprootBip32Derivation{
|
||||
{
|
||||
XOnlyPubKey: schnorr.SerializePubKey(
|
||||
&input.TaprootNUMSKey,
|
||||
),
|
||||
Bip32Path: []uint32{1, 2, 3},
|
||||
},
|
||||
}
|
||||
p.Inputs = append(p.Inputs, psbt.PInput{
|
||||
WitnessUtxo: &wire.TxOut{
|
||||
Value: fundAmt +
|
||||
int64(calcFee(1, 0, 1, 0, 0)),
|
||||
PkScript: p2trScript,
|
||||
},
|
||||
TaprootBip32Derivation: p2TrDerivations,
|
||||
})
|
||||
|
||||
return p
|
||||
}),
|
||||
changeIndex: -1,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
changeType: chanfunding.P2TRChangeAddress,
|
||||
expectedUtxoIndexes: []int{},
|
||||
expectChangeOutputIndex: -1,
|
||||
expectedFee: calcFee(1, 0, 1, 0, 0),
|
||||
}, {
|
||||
name: "large existing p2tr input, fee estimation existing " +
|
||||
"change output",
|
||||
utxos: []*lnwallet.Utxo{},
|
||||
packet: updatePacket(makePacket(&wire.TxOut{
|
||||
Value: fundAmt,
|
||||
PkScript: p2trScript,
|
||||
}), func(p *psbt.Packet) *psbt.Packet {
|
||||
p.UnsignedTx.TxIn = append(
|
||||
p.UnsignedTx.TxIn, &wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash{1, 2, 3},
|
||||
},
|
||||
},
|
||||
)
|
||||
p2TrDerivations := []*psbt.TaprootBip32Derivation{
|
||||
{
|
||||
XOnlyPubKey: schnorr.SerializePubKey(
|
||||
&input.TaprootNUMSKey,
|
||||
),
|
||||
Bip32Path: []uint32{1, 2, 3},
|
||||
},
|
||||
}
|
||||
p.Inputs = append(p.Inputs, psbt.PInput{
|
||||
WitnessUtxo: &wire.TxOut{
|
||||
Value: fundAmt * 2,
|
||||
PkScript: p2trScript,
|
||||
},
|
||||
TaprootBip32Derivation: p2TrDerivations,
|
||||
})
|
||||
|
||||
return p
|
||||
}),
|
||||
changeIndex: 0,
|
||||
feeRate: chainfee.FeePerKwFloor,
|
||||
changeType: chanfunding.ExistingChangeAddress,
|
||||
expectedUtxoIndexes: []int{},
|
||||
expectChangeOutputIndex: 0,
|
||||
expectedChangeOutputAmount: fundAmt*2 - calcFee(1, 0, 1, 0, 0),
|
||||
expectedFee: calcFee(1, 0, 1, 0, 0),
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
privKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
walletMock := &mock.WalletController{
|
||||
RootKey: privKey,
|
||||
Utxos: tc.utxos,
|
||||
}
|
||||
rpcServer, _, err := New(&Config{
|
||||
Wallet: walletMock,
|
||||
CoinSelectionLocker: &mockCoinSelectionLocker{},
|
||||
CoinSelectionStrategy: wallet.CoinSelectionLargest,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run(tc.name, func(tt *testing.T) {
|
||||
// To avoid our packet being mutated, we'll make a deep
|
||||
// copy of it, so we can still use the original in the
|
||||
// test case to compare the results to.
|
||||
var buf bytes.Buffer
|
||||
err := tc.packet.Serialize(&buf)
|
||||
require.NoError(tt, err)
|
||||
|
||||
copiedPacket, err := psbt.NewFromRawBytes(&buf, false)
|
||||
require.NoError(tt, err)
|
||||
|
||||
resp, err := rpcServer.fundPsbtCoinSelect(
|
||||
"", tc.changeIndex, copiedPacket, 0,
|
||||
tc.changeType, tc.feeRate,
|
||||
)
|
||||
|
||||
switch {
|
||||
case tc.expectedErr != nil:
|
||||
require.Error(tt, err)
|
||||
require.ErrorIs(tt, err, tc.expectedErr)
|
||||
|
||||
return
|
||||
|
||||
case tc.expectedErrType != nil:
|
||||
require.Error(tt, err)
|
||||
require.ErrorAs(tt, err, &tc.expectedErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(tt, err)
|
||||
require.NotNil(tt, resp)
|
||||
|
||||
resultPacket, err := psbt.NewFromRawBytes(
|
||||
bytes.NewReader(resp.FundedPsbt), false,
|
||||
)
|
||||
require.NoError(tt, err)
|
||||
resultTx := resultPacket.UnsignedTx
|
||||
|
||||
expectedNumInputs := len(tc.expectedUtxoIndexes) +
|
||||
len(tc.packet.Inputs)
|
||||
require.Len(tt, resultPacket.Inputs, expectedNumInputs)
|
||||
require.Len(tt, resultTx.TxIn, expectedNumInputs)
|
||||
require.Equal(
|
||||
tt, tc.expectChangeOutputIndex,
|
||||
resp.ChangeOutputIndex,
|
||||
)
|
||||
|
||||
fee, err := resultPacket.GetTxFee()
|
||||
require.NoError(tt, err)
|
||||
require.EqualValues(tt, tc.expectedFee, fee)
|
||||
|
||||
if tc.expectedChangeOutputAmount != 0 {
|
||||
changeIdx := resp.ChangeOutputIndex
|
||||
require.GreaterOrEqual(tt, changeIdx, int32(-1))
|
||||
require.Less(
|
||||
tt, changeIdx,
|
||||
int32(len(resultTx.TxOut)),
|
||||
)
|
||||
|
||||
changeOut := resultTx.TxOut[changeIdx]
|
||||
|
||||
require.EqualValues(
|
||||
tt, tc.expectedChangeOutputAmount,
|
||||
changeOut.Value,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue