chanfunding+lnwallet: move coin selection code into new chanfunding package
In this commit, we make an incremental change to move the existing coin selection code into a new chanfunding package. In later commits, this package will grow to serve all the lower level channel funding needs in the daemon.
This commit is contained in:
parent
f9d22cd900
commit
9eefdef262
216
lnwallet/chanfunding/coin_select.go
Normal file
216
lnwallet/chanfunding/coin_select.go
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
package chanfunding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrInsufficientFunds is a type matching the error interface which is
|
||||||
|
// returned when coin selection for a new funding transaction fails to due
|
||||||
|
// having an insufficient amount of confirmed funds.
|
||||||
|
type ErrInsufficientFunds struct {
|
||||||
|
amountAvailable btcutil.Amount
|
||||||
|
amountSelected btcutil.Amount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns a human readable string describing the error.
|
||||||
|
func (e *ErrInsufficientFunds) Error() string {
|
||||||
|
return fmt.Sprintf("not enough witness outputs to create funding "+
|
||||||
|
"transaction, need %v only have %v available",
|
||||||
|
e.amountAvailable, e.amountSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coin represents a spendable UTXO which is available for channel funding.
|
||||||
|
// This UTXO need not reside in our internal wallet as an example, and instead
|
||||||
|
// may be derived from an existing watch-only wallet. It wraps both the output
|
||||||
|
// present within the UTXO set, and also the outpoint that generates this coin.
|
||||||
|
type Coin struct {
|
||||||
|
wire.TxOut
|
||||||
|
|
||||||
|
wire.OutPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectInputs selects a slice of inputs necessary to meet the specified
|
||||||
|
// selection amount. If input selection is unable to succeed due to insufficient
|
||||||
|
// funds, a non-nil error is returned. Additionally, the total amount of the
|
||||||
|
// selected coins are returned in order for the caller to properly handle
|
||||||
|
// change+fees.
|
||||||
|
func selectInputs(amt btcutil.Amount, coins []Coin) (btcutil.Amount, []Coin, error) {
|
||||||
|
satSelected := btcutil.Amount(0)
|
||||||
|
for i, coin := range coins {
|
||||||
|
satSelected += btcutil.Amount(coin.Value)
|
||||||
|
if satSelected >= amt {
|
||||||
|
return satSelected, coins[:i+1], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, nil, &ErrInsufficientFunds{amt, satSelected}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoinSelect attempts to select a sufficient amount of coins, including a
|
||||||
|
// change output to fund amt satoshis, adhering to the specified fee rate. The
|
||||||
|
// specified fee rate should be expressed in sat/kw for coin selection to
|
||||||
|
// function properly.
|
||||||
|
func CoinSelect(feeRate chainfee.SatPerKWeight, amt btcutil.Amount,
|
||||||
|
coins []Coin) ([]Coin, btcutil.Amount, error) {
|
||||||
|
|
||||||
|
amtNeeded := amt
|
||||||
|
for {
|
||||||
|
// First perform an initial round of coin selection to estimate
|
||||||
|
// the required fee.
|
||||||
|
totalSat, selectedUtxos, err := selectInputs(amtNeeded, coins)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var weightEstimate input.TxWeightEstimator
|
||||||
|
|
||||||
|
for _, utxo := range selectedUtxos {
|
||||||
|
switch {
|
||||||
|
|
||||||
|
case txscript.IsPayToWitnessPubKeyHash(utxo.PkScript):
|
||||||
|
weightEstimate.AddP2WKHInput()
|
||||||
|
|
||||||
|
case txscript.IsPayToScriptHash(utxo.PkScript):
|
||||||
|
weightEstimate.AddNestedP2WKHInput()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, 0, fmt.Errorf("unsupported address type: %x",
|
||||||
|
utxo.PkScript)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel funding multisig output is P2WSH.
|
||||||
|
weightEstimate.AddP2WSHOutput()
|
||||||
|
|
||||||
|
// Assume that change output is a P2WKH output.
|
||||||
|
//
|
||||||
|
// TODO: Handle wallets that generate non-witness change
|
||||||
|
// addresses.
|
||||||
|
// TODO(halseth): make coinSelect not estimate change output
|
||||||
|
// for dust change.
|
||||||
|
weightEstimate.AddP2WKHOutput()
|
||||||
|
|
||||||
|
// The difference between the selected amount and the amount
|
||||||
|
// requested will be used to pay fees, and generate a change
|
||||||
|
// output with the remaining.
|
||||||
|
overShootAmt := totalSat - amt
|
||||||
|
|
||||||
|
// Based on the estimated size and fee rate, if the excess
|
||||||
|
// amount isn't enough to pay fees, then increase the requested
|
||||||
|
// coin amount by the estimate required fee, performing another
|
||||||
|
// round of coin selection.
|
||||||
|
totalWeight := int64(weightEstimate.Weight())
|
||||||
|
requiredFee := feeRate.FeeForWeight(totalWeight)
|
||||||
|
if overShootAmt < requiredFee {
|
||||||
|
amtNeeded = amt + requiredFee
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the fee is sufficient, then calculate the size of the
|
||||||
|
// change output.
|
||||||
|
changeAmt := overShootAmt - requiredFee
|
||||||
|
|
||||||
|
return selectedUtxos, changeAmt, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoinSelectSubtractFees attempts to select coins such that we'll spend up to
|
||||||
|
// amt in total after fees, adhering to the specified fee rate. The selected
|
||||||
|
// coins, the final output and change values are returned.
|
||||||
|
func CoinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt,
|
||||||
|
dustLimit btcutil.Amount, coins []Coin) ([]Coin, btcutil.Amount,
|
||||||
|
btcutil.Amount, error) {
|
||||||
|
|
||||||
|
// First perform an initial round of coin selection to estimate
|
||||||
|
// the required fee.
|
||||||
|
totalSat, selectedUtxos, err := selectInputs(amt, coins)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var weightEstimate input.TxWeightEstimator
|
||||||
|
for _, utxo := range selectedUtxos {
|
||||||
|
switch {
|
||||||
|
|
||||||
|
case txscript.IsPayToWitnessPubKeyHash(utxo.PkScript):
|
||||||
|
weightEstimate.AddP2WKHInput()
|
||||||
|
|
||||||
|
case txscript.IsPayToScriptHash(utxo.PkScript):
|
||||||
|
weightEstimate.AddNestedP2WKHInput()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, 0, 0, fmt.Errorf("unsupported address "+
|
||||||
|
"type: %x", utxo.PkScript)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel funding multisig output is P2WSH.
|
||||||
|
weightEstimate.AddP2WSHOutput()
|
||||||
|
|
||||||
|
// At this point we've got two possibilities, either create a
|
||||||
|
// change output, or not. We'll first try without creating a
|
||||||
|
// change output.
|
||||||
|
//
|
||||||
|
// Estimate the fee required for a transaction without a change
|
||||||
|
// output.
|
||||||
|
totalWeight := int64(weightEstimate.Weight())
|
||||||
|
requiredFee := feeRate.FeeForWeight(totalWeight)
|
||||||
|
|
||||||
|
// For a transaction without a change output, we'll let everything go
|
||||||
|
// to our multi-sig output after subtracting fees.
|
||||||
|
outputAmt := totalSat - requiredFee
|
||||||
|
changeAmt := btcutil.Amount(0)
|
||||||
|
|
||||||
|
// If the the output is too small after subtracting the fee, the coin
|
||||||
|
// selection cannot be performed with an amount this small.
|
||||||
|
if outputAmt <= dustLimit {
|
||||||
|
return nil, 0, 0, fmt.Errorf("output amount(%v) after "+
|
||||||
|
"subtracting fees(%v) below dust limit(%v)", outputAmt,
|
||||||
|
requiredFee, dustLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We were able to create a transaction with no change from the
|
||||||
|
// selected inputs. We'll remember the resulting values for
|
||||||
|
// now, while we try to add a change output. Assume that change output
|
||||||
|
// is a P2WKH output.
|
||||||
|
weightEstimate.AddP2WKHOutput()
|
||||||
|
|
||||||
|
// Now that we have added the change output, redo the fee
|
||||||
|
// estimate.
|
||||||
|
totalWeight = int64(weightEstimate.Weight())
|
||||||
|
requiredFee = feeRate.FeeForWeight(totalWeight)
|
||||||
|
|
||||||
|
// For a transaction with a change output, everything we don't spend
|
||||||
|
// will go to change.
|
||||||
|
newChange := totalSat - amt
|
||||||
|
newOutput := amt - requiredFee
|
||||||
|
|
||||||
|
// If adding a change output leads to both outputs being above
|
||||||
|
// the dust limit, we'll add the change output. Otherwise we'll
|
||||||
|
// go with the no change tx we originally found.
|
||||||
|
if newChange > dustLimit && newOutput > dustLimit {
|
||||||
|
outputAmt = newOutput
|
||||||
|
changeAmt = newChange
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check the resulting output values to make sure we
|
||||||
|
// don't burn a great part to fees.
|
||||||
|
totalOut := outputAmt + changeAmt
|
||||||
|
fee := totalSat - totalOut
|
||||||
|
|
||||||
|
// Fail if more than 20% goes to fees.
|
||||||
|
// TODO(halseth): smarter fee limit. Make configurable or dynamic wrt
|
||||||
|
// total funding size?
|
||||||
|
if fee > totalOut/5 {
|
||||||
|
return nil, 0, 0, fmt.Errorf("fee %v on total output"+
|
||||||
|
"value %v", fee, totalOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedUtxos, outputAmt, changeAmt, nil
|
||||||
|
}
|
@ -1,13 +1,21 @@
|
|||||||
package lnwallet
|
package chanfunding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
p2wkhScript, _ = hex.DecodeString(
|
||||||
|
"001411034bdcb6ccb7744fdfdeea958a6fb0b415a032",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// fundingFee is a helper method that returns the fee estimate used for a tx
|
// fundingFee is a helper method that returns the fee estimate used for a tx
|
||||||
// with the given number of inputs and the optional change output. This matches
|
// with the given number of inputs and the optional change output. This matches
|
||||||
// the estimate done by the wallet.
|
// the estimate done by the wallet.
|
||||||
@ -48,7 +56,7 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
outputValue btcutil.Amount
|
outputValue btcutil.Amount
|
||||||
coins []*Utxo
|
coins []Coin
|
||||||
|
|
||||||
expectedInput []btcutil.Amount
|
expectedInput []btcutil.Amount
|
||||||
expectedChange btcutil.Amount
|
expectedChange btcutil.Amount
|
||||||
@ -61,12 +69,14 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
// This will obviously lead to a change output of
|
// This will obviously lead to a change output of
|
||||||
// almost 0.5 BTC.
|
// almost 0.5 BTC.
|
||||||
name: "big change",
|
name: "big change",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
|
PkScript: p2wkhScript,
|
||||||
Value: 1 * btcutil.SatoshiPerBitcoin,
|
Value: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
outputValue: 0.5 * btcutil.SatoshiPerBitcoin,
|
outputValue: 0.5 * btcutil.SatoshiPerBitcoin,
|
||||||
|
|
||||||
// The one and only input will be selected.
|
// The one and only input will be selected.
|
||||||
@ -81,12 +91,14 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
// This should lead to an error, as we don't have
|
// This should lead to an error, as we don't have
|
||||||
// enough funds to pay the fee.
|
// enough funds to pay the fee.
|
||||||
name: "nothing left for fees",
|
name: "nothing left for fees",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
|
PkScript: p2wkhScript,
|
||||||
Value: 1 * btcutil.SatoshiPerBitcoin,
|
Value: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
outputValue: 1 * btcutil.SatoshiPerBitcoin,
|
outputValue: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
@ -95,12 +107,14 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
// as big as possible, such that the remaining change
|
// as big as possible, such that the remaining change
|
||||||
// will be dust.
|
// will be dust.
|
||||||
name: "dust change",
|
name: "dust change",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
|
PkScript: p2wkhScript,
|
||||||
Value: 1 * btcutil.SatoshiPerBitcoin,
|
Value: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
// We tune the output value by subtracting the expected
|
// We tune the output value by subtracting the expected
|
||||||
// fee and a small dust amount.
|
// fee and a small dust amount.
|
||||||
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true) - dust,
|
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true) - dust,
|
||||||
@ -117,12 +131,14 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
// as big as possible, such that there is nothing left
|
// as big as possible, such that there is nothing left
|
||||||
// for change.
|
// for change.
|
||||||
name: "no change",
|
name: "no change",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
|
PkScript: p2wkhScript,
|
||||||
Value: 1 * btcutil.SatoshiPerBitcoin,
|
Value: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
// We tune the output value to be the maximum amount
|
// We tune the output value to be the maximum amount
|
||||||
// possible, leaving just enough for fees.
|
// possible, leaving just enough for fees.
|
||||||
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true),
|
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true),
|
||||||
@ -143,7 +159,7 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
selected, changeAmt, err := coinSelect(
|
selected, changeAmt, err := CoinSelect(
|
||||||
feeRate, test.outputValue, test.coins,
|
feeRate, test.outputValue, test.coins,
|
||||||
)
|
)
|
||||||
if !test.expectErr && err != nil {
|
if !test.expectErr && err != nil {
|
||||||
@ -166,7 +182,7 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, coin := range selected {
|
for i, coin := range selected {
|
||||||
if coin.Value != test.expectedInput[i] {
|
if coin.Value != int64(test.expectedInput[i]) {
|
||||||
t.Fatalf("expected input %v to have value %v, "+
|
t.Fatalf("expected input %v to have value %v, "+
|
||||||
"had %v", i, test.expectedInput[i],
|
"had %v", i, test.expectedInput[i],
|
||||||
coin.Value)
|
coin.Value)
|
||||||
@ -195,7 +211,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
spendValue btcutil.Amount
|
spendValue btcutil.Amount
|
||||||
coins []*Utxo
|
coins []Coin
|
||||||
|
|
||||||
expectedInput []btcutil.Amount
|
expectedInput []btcutil.Amount
|
||||||
expectedFundingAmt btcutil.Amount
|
expectedFundingAmt btcutil.Amount
|
||||||
@ -209,12 +225,14 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
// should lead to a funding TX with one output, the
|
// should lead to a funding TX with one output, the
|
||||||
// rest goes to fees.
|
// rest goes to fees.
|
||||||
name: "spend all",
|
name: "spend all",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
|
PkScript: p2wkhScript,
|
||||||
Value: 1 * btcutil.SatoshiPerBitcoin,
|
Value: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
spendValue: 1 * btcutil.SatoshiPerBitcoin,
|
spendValue: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
|
|
||||||
// The one and only input will be selected.
|
// The one and only input will be selected.
|
||||||
@ -228,10 +246,12 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
// The total funds available is below the dust limit
|
// The total funds available is below the dust limit
|
||||||
// after paying fees.
|
// after paying fees.
|
||||||
name: "dust output",
|
name: "dust output",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
Value: fundingFee(feeRate, 1, false) + dust,
|
PkScript: p2wkhScript,
|
||||||
|
Value: int64(fundingFee(feeRate, 1, false) + dust),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
spendValue: fundingFee(feeRate, 1, false) + dust,
|
spendValue: fundingFee(feeRate, 1, false) + dust,
|
||||||
@ -243,12 +263,14 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
// is below the dust limit. The remainder should go
|
// is below the dust limit. The remainder should go
|
||||||
// towards the funding output.
|
// towards the funding output.
|
||||||
name: "dust change",
|
name: "dust change",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
|
PkScript: p2wkhScript,
|
||||||
Value: 1 * btcutil.SatoshiPerBitcoin,
|
Value: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
spendValue: 1*btcutil.SatoshiPerBitcoin - dust,
|
spendValue: 1*btcutil.SatoshiPerBitcoin - dust,
|
||||||
|
|
||||||
expectedInput: []btcutil.Amount{
|
expectedInput: []btcutil.Amount{
|
||||||
@ -260,10 +282,12 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// We got just enough funds to create an output above the dust limit.
|
// We got just enough funds to create an output above the dust limit.
|
||||||
name: "output right above dustlimit",
|
name: "output right above dustlimit",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
Value: fundingFee(feeRate, 1, false) + dustLimit + 1,
|
PkScript: p2wkhScript,
|
||||||
|
Value: int64(fundingFee(feeRate, 1, false) + dustLimit + 1),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
spendValue: fundingFee(feeRate, 1, false) + dustLimit + 1,
|
spendValue: fundingFee(feeRate, 1, false) + dustLimit + 1,
|
||||||
@ -278,10 +302,12 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
// Amount left is below dust limit after paying fee for
|
// Amount left is below dust limit after paying fee for
|
||||||
// a change output, resulting in a no-change tx.
|
// a change output, resulting in a no-change tx.
|
||||||
name: "no amount to pay fee for change",
|
name: "no amount to pay fee for change",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
Value: fundingFee(feeRate, 1, false) + 2*(dustLimit+1),
|
PkScript: p2wkhScript,
|
||||||
|
Value: int64(fundingFee(feeRate, 1, false) + 2*(dustLimit+1)),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
spendValue: fundingFee(feeRate, 1, false) + dustLimit + 1,
|
spendValue: fundingFee(feeRate, 1, false) + dustLimit + 1,
|
||||||
@ -295,10 +321,12 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// If more than 20% of funds goes to fees, it should fail.
|
// If more than 20% of funds goes to fees, it should fail.
|
||||||
name: "high fee",
|
name: "high fee",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
Value: 5 * fundingFee(feeRate, 1, false),
|
PkScript: p2wkhScript,
|
||||||
|
Value: int64(5 * fundingFee(feeRate, 1, false)),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
spendValue: 5 * fundingFee(feeRate, 1, false),
|
spendValue: 5 * fundingFee(feeRate, 1, false),
|
||||||
@ -308,8 +336,10 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
selected, localFundingAmt, changeAmt, err := coinSelectSubtractFees(
|
selected, localFundingAmt, changeAmt, err := CoinSelectSubtractFees(
|
||||||
feeRate, test.spendValue, dustLimit, test.coins,
|
feeRate, test.spendValue, dustLimit, test.coins,
|
||||||
)
|
)
|
||||||
if !test.expectErr && err != nil {
|
if !test.expectErr && err != nil {
|
||||||
@ -332,7 +362,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, coin := range selected {
|
for i, coin := range selected {
|
||||||
if coin.Value != test.expectedInput[i] {
|
if coin.Value != int64(test.expectedInput[i]) {
|
||||||
t.Fatalf("expected input %v to have value %v, "+
|
t.Fatalf("expected input %v to have value %v, "+
|
||||||
"had %v", i, test.expectedInput[i],
|
"had %v", i, test.expectedInput[i],
|
||||||
coin.Value)
|
coin.Value)
|
@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chanvalidate"
|
"github.com/lightningnetwork/lnd/lnwallet/chanvalidate"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/shachain"
|
"github.com/lightningnetwork/lnd/shachain"
|
||||||
@ -33,20 +34,6 @@ const (
|
|||||||
msgBufferSize = 100
|
msgBufferSize = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInsufficientFunds is a type matching the error interface which is
|
|
||||||
// returned when coin selection for a new funding transaction fails to due
|
|
||||||
// having an insufficient amount of confirmed funds.
|
|
||||||
type ErrInsufficientFunds struct {
|
|
||||||
amountAvailable btcutil.Amount
|
|
||||||
amountSelected btcutil.Amount
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ErrInsufficientFunds) Error() string {
|
|
||||||
return fmt.Sprintf("not enough witness outputs to create funding transaction,"+
|
|
||||||
" need %v only have %v available", e.amountAvailable,
|
|
||||||
e.amountSelected)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitFundingReserveMsg is the first message sent to initiate the workflow
|
// InitFundingReserveMsg is the first message sent to initiate the workflow
|
||||||
// required to open a payment channel with a remote peer. The initial required
|
// required to open a payment channel with a remote peer. The initial required
|
||||||
// parameters are configurable across channels. These parameters are to be
|
// parameters are configurable across channels. These parameters are to be
|
||||||
@ -1341,13 +1328,24 @@ func (l *LightningWallet) selectCoinsAndChange(feeRate chainfee.SatPerKWeight,
|
|||||||
|
|
||||||
// Find all unlocked unspent witness outputs that satisfy the minimum
|
// Find all unlocked unspent witness outputs that satisfy the minimum
|
||||||
// number of confirmations required.
|
// number of confirmations required.
|
||||||
coins, err := l.ListUnspentWitness(minConfs, math.MaxInt32)
|
utxos, err := l.ListUnspentWitness(minConfs, math.MaxInt32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
coins := make([]chanfunding.Coin, len(utxos), 0)
|
||||||
|
for _, utxo := range utxos {
|
||||||
|
coins = append(coins, chanfunding.Coin{
|
||||||
|
TxOut: wire.TxOut{
|
||||||
|
Value: int64(utxo.Value),
|
||||||
|
PkScript: utxo.PkScript,
|
||||||
|
},
|
||||||
|
OutPoint: utxo.OutPoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
selectedCoins []*Utxo
|
selectedCoins []chanfunding.Coin
|
||||||
fundingAmt btcutil.Amount
|
fundingAmt btcutil.Amount
|
||||||
changeAmt btcutil.Amount
|
changeAmt btcutil.Amount
|
||||||
)
|
)
|
||||||
@ -1361,7 +1359,7 @@ func (l *LightningWallet) selectCoinsAndChange(feeRate chainfee.SatPerKWeight,
|
|||||||
// won't deduct more that the specified balance from our wallet.
|
// won't deduct more that the specified balance from our wallet.
|
||||||
case subtractFees:
|
case subtractFees:
|
||||||
dustLimit := l.Cfg.DefaultConstraints.DustLimit
|
dustLimit := l.Cfg.DefaultConstraints.DustLimit
|
||||||
selectedCoins, fundingAmt, changeAmt, err = coinSelectSubtractFees(
|
selectedCoins, fundingAmt, changeAmt, err = chanfunding.CoinSelectSubtractFees(
|
||||||
feeRate, amt, dustLimit, coins,
|
feeRate, amt, dustLimit, coins,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1372,7 +1370,7 @@ func (l *LightningWallet) selectCoinsAndChange(feeRate chainfee.SatPerKWeight,
|
|||||||
// amount.
|
// amount.
|
||||||
default:
|
default:
|
||||||
fundingAmt = amt
|
fundingAmt = amt
|
||||||
selectedCoins, changeAmt, err = coinSelect(feeRate, amt, coins)
|
selectedCoins, changeAmt, err = chanfunding.CoinSelect(feeRate, amt, coins)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1468,179 +1466,6 @@ func initStateHints(commit1, commit2 *wire.MsgTx,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectInputs selects a slice of inputs necessary to meet the specified
|
|
||||||
// selection amount. If input selection is unable to succeed due to insufficient
|
|
||||||
// funds, a non-nil error is returned. Additionally, the total amount of the
|
|
||||||
// selected coins are returned in order for the caller to properly handle
|
|
||||||
// change+fees.
|
|
||||||
func selectInputs(amt btcutil.Amount, coins []*Utxo) (btcutil.Amount, []*Utxo, error) {
|
|
||||||
satSelected := btcutil.Amount(0)
|
|
||||||
for i, coin := range coins {
|
|
||||||
satSelected += coin.Value
|
|
||||||
if satSelected >= amt {
|
|
||||||
return satSelected, coins[:i+1], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, nil, &ErrInsufficientFunds{amt, satSelected}
|
|
||||||
}
|
|
||||||
|
|
||||||
// coinSelect attempts to select a sufficient amount of coins, including a
|
|
||||||
// change output to fund amt satoshis, adhering to the specified fee rate. The
|
|
||||||
// specified fee rate should be expressed in sat/kw for coin selection to
|
|
||||||
// function properly.
|
|
||||||
func coinSelect(feeRate chainfee.SatPerKWeight, amt btcutil.Amount,
|
|
||||||
coins []*Utxo) ([]*Utxo, btcutil.Amount, error) {
|
|
||||||
|
|
||||||
amtNeeded := amt
|
|
||||||
for {
|
|
||||||
// First perform an initial round of coin selection to estimate
|
|
||||||
// the required fee.
|
|
||||||
totalSat, selectedUtxos, err := selectInputs(amtNeeded, coins)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var weightEstimate input.TxWeightEstimator
|
|
||||||
|
|
||||||
for _, utxo := range selectedUtxos {
|
|
||||||
switch utxo.AddressType {
|
|
||||||
case WitnessPubKey:
|
|
||||||
weightEstimate.AddP2WKHInput()
|
|
||||||
case NestedWitnessPubKey:
|
|
||||||
weightEstimate.AddNestedP2WKHInput()
|
|
||||||
default:
|
|
||||||
return nil, 0, fmt.Errorf("unsupported address type: %v",
|
|
||||||
utxo.AddressType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channel funding multisig output is P2WSH.
|
|
||||||
weightEstimate.AddP2WSHOutput()
|
|
||||||
|
|
||||||
// Assume that change output is a P2WKH output.
|
|
||||||
//
|
|
||||||
// TODO: Handle wallets that generate non-witness change
|
|
||||||
// addresses.
|
|
||||||
// TODO(halseth): make coinSelect not estimate change output
|
|
||||||
// for dust change.
|
|
||||||
weightEstimate.AddP2WKHOutput()
|
|
||||||
|
|
||||||
// The difference between the selected amount and the amount
|
|
||||||
// requested will be used to pay fees, and generate a change
|
|
||||||
// output with the remaining.
|
|
||||||
overShootAmt := totalSat - amt
|
|
||||||
|
|
||||||
// Based on the estimated size and fee rate, if the excess
|
|
||||||
// amount isn't enough to pay fees, then increase the requested
|
|
||||||
// coin amount by the estimate required fee, performing another
|
|
||||||
// round of coin selection.
|
|
||||||
totalWeight := int64(weightEstimate.Weight())
|
|
||||||
requiredFee := feeRate.FeeForWeight(totalWeight)
|
|
||||||
if overShootAmt < requiredFee {
|
|
||||||
amtNeeded = amt + requiredFee
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the fee is sufficient, then calculate the size of the
|
|
||||||
// change output.
|
|
||||||
changeAmt := overShootAmt - requiredFee
|
|
||||||
|
|
||||||
return selectedUtxos, changeAmt, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// coinSelectSubtractFees attempts to select coins such that we'll spend up to
|
|
||||||
// amt in total after fees, adhering to the specified fee rate. The selected
|
|
||||||
// coins, the final output and change values are returned.
|
|
||||||
func coinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt,
|
|
||||||
dustLimit btcutil.Amount, coins []*Utxo) ([]*Utxo, btcutil.Amount,
|
|
||||||
btcutil.Amount, error) {
|
|
||||||
|
|
||||||
// First perform an initial round of coin selection to estimate
|
|
||||||
// the required fee.
|
|
||||||
totalSat, selectedUtxos, err := selectInputs(amt, coins)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var weightEstimate input.TxWeightEstimator
|
|
||||||
for _, utxo := range selectedUtxos {
|
|
||||||
switch utxo.AddressType {
|
|
||||||
case WitnessPubKey:
|
|
||||||
weightEstimate.AddP2WKHInput()
|
|
||||||
case NestedWitnessPubKey:
|
|
||||||
weightEstimate.AddNestedP2WKHInput()
|
|
||||||
default:
|
|
||||||
return nil, 0, 0, fmt.Errorf("unsupported "+
|
|
||||||
"address type: %v", utxo.AddressType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channel funding multisig output is P2WSH.
|
|
||||||
weightEstimate.AddP2WSHOutput()
|
|
||||||
|
|
||||||
// At this point we've got two possibilities, either create a
|
|
||||||
// change output, or not. We'll first try without creating a
|
|
||||||
// change output.
|
|
||||||
//
|
|
||||||
// Estimate the fee required for a transaction without a change
|
|
||||||
// output.
|
|
||||||
totalWeight := int64(weightEstimate.Weight())
|
|
||||||
requiredFee := feeRate.FeeForWeight(totalWeight)
|
|
||||||
|
|
||||||
// For a transaction without a change output, we'll let everything go
|
|
||||||
// to our multi-sig output after subtracting fees.
|
|
||||||
outputAmt := totalSat - requiredFee
|
|
||||||
changeAmt := btcutil.Amount(0)
|
|
||||||
|
|
||||||
// If the the output is too small after subtracting the fee, the coin
|
|
||||||
// selection cannot be performed with an amount this small.
|
|
||||||
if outputAmt <= dustLimit {
|
|
||||||
return nil, 0, 0, fmt.Errorf("output amount(%v) after "+
|
|
||||||
"subtracting fees(%v) below dust limit(%v)", outputAmt,
|
|
||||||
requiredFee, dustLimit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We were able to create a transaction with no change from the
|
|
||||||
// selected inputs. We'll remember the resulting values for
|
|
||||||
// now, while we try to add a change output. Assume that change output
|
|
||||||
// is a P2WKH output.
|
|
||||||
weightEstimate.AddP2WKHOutput()
|
|
||||||
|
|
||||||
// Now that we have added the change output, redo the fee
|
|
||||||
// estimate.
|
|
||||||
totalWeight = int64(weightEstimate.Weight())
|
|
||||||
requiredFee = feeRate.FeeForWeight(totalWeight)
|
|
||||||
|
|
||||||
// For a transaction with a change output, everything we don't spend
|
|
||||||
// will go to change.
|
|
||||||
newChange := totalSat - amt
|
|
||||||
newOutput := amt - requiredFee
|
|
||||||
|
|
||||||
// If adding a change output leads to both outputs being above
|
|
||||||
// the dust limit, we'll add the change output. Otherwise we'll
|
|
||||||
// go with the no change tx we originally found.
|
|
||||||
if newChange > dustLimit && newOutput > dustLimit {
|
|
||||||
outputAmt = newOutput
|
|
||||||
changeAmt = newChange
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanity check the resulting output values to make sure we
|
|
||||||
// don't burn a great part to fees.
|
|
||||||
totalOut := outputAmt + changeAmt
|
|
||||||
fee := totalSat - totalOut
|
|
||||||
|
|
||||||
// Fail if more than 20% goes to fees.
|
|
||||||
// TODO(halseth): smarter fee limit. Make configurable or dynamic wrt
|
|
||||||
// total funding size?
|
|
||||||
if fee > totalOut/5 {
|
|
||||||
return nil, 0, 0, fmt.Errorf("fee %v on total output"+
|
|
||||||
"value %v", fee, totalOut)
|
|
||||||
}
|
|
||||||
|
|
||||||
return selectedUtxos, outputAmt, changeAmt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateChannel will attempt to fully validate a newly mined channel, given
|
// ValidateChannel will attempt to fully validate a newly mined channel, given
|
||||||
// its funding transaction and existing channel state. If this method returns
|
// its funding transaction and existing channel state. If this method returns
|
||||||
// an error, then the mined channel is invalid, and shouldn't be used.
|
// an error, then the mined channel is invalid, and shouldn't be used.
|
||||||
|
Loading…
Reference in New Issue
Block a user