217 lines
6.9 KiB
Go
217 lines
6.9 KiB
Go
|
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
|
||
|
}
|