github.com/decred/dcrlnd@v0.7.6/lnwallet/chanfunding/coin_select.go (about)

     1  package chanfunding
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/decred/dcrd/dcrutil/v4"
     7  	"github.com/decred/dcrd/txscript/v4/stdscript"
     8  	"github.com/decred/dcrd/wire"
     9  	"github.com/decred/dcrlnd/input"
    10  	"github.com/decred/dcrlnd/lnwallet/chainfee"
    11  )
    12  
    13  // ErrInsufficientFunds is a type matching the error interface which is
    14  // returned when coin selection for a new funding transaction fails to due
    15  // having an insufficient amount of confirmed funds.
    16  type ErrInsufficientFunds struct {
    17  	amountAvailable dcrutil.Amount
    18  	amountSelected  dcrutil.Amount
    19  }
    20  
    21  // Error returns a human readable string describing the error.
    22  func (e *ErrInsufficientFunds) Error() string {
    23  	return fmt.Sprintf("not enough witness outputs to create funding "+
    24  		"transaction, need %v only have %v  available",
    25  		e.amountAvailable, e.amountSelected)
    26  }
    27  
    28  // errUnsupportedInput is a type matching the error interface, which is returned
    29  // when trying to calculate the fee of a transaction that references an
    30  // unsupported script in the outpoint of a transaction input.
    31  type errUnsupportedInput struct {
    32  	PkScript []byte
    33  }
    34  
    35  // Error returns a human readable string describing the error.
    36  func (e *errUnsupportedInput) Error() string {
    37  	return fmt.Sprintf("unsupported address type: %x", e.PkScript)
    38  }
    39  
    40  // Coin represents a spendable UTXO which is available for channel funding.
    41  // This UTXO need not reside in our internal wallet as an example, and instead
    42  // may be derived from an existing watch-only wallet. It wraps both the output
    43  // present within the UTXO set, and also the outpoint that generates this coin.
    44  type Coin struct {
    45  	wire.TxOut
    46  
    47  	wire.OutPoint
    48  }
    49  
    50  // selectInputs selects a slice of inputs necessary to meet the specified
    51  // selection amount. If input selection is unable to succeed due to insufficient
    52  // funds, a non-nil error is returned. Additionally, the total amount of the
    53  // selected coins are returned in order for the caller to properly handle
    54  // change+fees.
    55  func selectInputs(amt dcrutil.Amount, coins []Coin) (dcrutil.Amount, []Coin, error) {
    56  	atomSelected := dcrutil.Amount(0)
    57  	for i, coin := range coins {
    58  		atomSelected += dcrutil.Amount(coin.Value)
    59  		if atomSelected >= amt {
    60  			return atomSelected, coins[:i+1], nil
    61  		}
    62  	}
    63  
    64  	return 0, nil, &ErrInsufficientFunds{amt, atomSelected}
    65  }
    66  
    67  // calculateFees returns for the specified utxos and fee rate two fee
    68  // estimates, one calculated using a change output and one without. The weight
    69  // added to the estimator from a change output is for a P2WKH output.
    70  func calculateFees(utxos []Coin, feeRate chainfee.AtomPerKByte) (dcrutil.Amount,
    71  	dcrutil.Amount, error) {
    72  
    73  	var sizeEstimate input.TxSizeEstimator
    74  	for _, utxo := range utxos {
    75  		scriptClass := stdscript.DetermineScriptType(utxo.Version,
    76  			utxo.PkScript)
    77  
    78  		switch scriptClass {
    79  		case stdscript.STPubKeyHashEcdsaSecp256k1:
    80  			sizeEstimate.AddP2PKHInput()
    81  		default:
    82  			return 0, 0, &errUnsupportedInput{utxo.PkScript}
    83  		}
    84  	}
    85  
    86  	// Channel funding multisig output is P2SH.
    87  	sizeEstimate.AddP2SHOutput()
    88  
    89  	// Estimate the fee required for a transaction without a change
    90  	// output.
    91  	totalSize := sizeEstimate.Size()
    92  	requiredFeeNoChange := feeRate.FeeForSize(totalSize)
    93  
    94  	// Estimate the fee required for a transaction with a change output.
    95  	// Assume that change output is a P2PKH output.
    96  	sizeEstimate.AddP2PKHOutput()
    97  
    98  	// Now that we have added the change output, redo the fee
    99  	// estimate.
   100  	totalSize = sizeEstimate.Size()
   101  	requiredFeeWithChange := feeRate.FeeForSize(totalSize)
   102  
   103  	return requiredFeeNoChange, requiredFeeWithChange, nil
   104  }
   105  
   106  // sanityCheckFee checks if the specified fee amounts to over 20% of the total
   107  // output amount and raises an error.
   108  func sanityCheckFee(totalOut, fee dcrutil.Amount) error {
   109  	// Fail if more than 20% goes to fees.
   110  	// TODO(halseth): smarter fee limit. Make configurable or dynamic wrt
   111  	// total funding size?
   112  	if fee > totalOut/5 {
   113  		return fmt.Errorf("fee %v on total output value %v", fee,
   114  			totalOut)
   115  	}
   116  	return nil
   117  }
   118  
   119  // CoinSelect attempts to select a sufficient amount of coins, including a
   120  // change output to fund amt satoshis, adhering to the specified fee rate. The
   121  // specified fee rate should be expressed in sat/kw for coin selection to
   122  // function properly.
   123  func CoinSelect(feeRate chainfee.AtomPerKByte, amt, dustLimit dcrutil.Amount,
   124  	coins []Coin) ([]Coin, dcrutil.Amount, error) {
   125  
   126  	amtNeeded := amt
   127  	for {
   128  		// First perform an initial round of coin selection to estimate
   129  		// the required fee.
   130  		totalAtoms, selectedUtxos, err := selectInputs(amtNeeded, coins)
   131  		if err != nil {
   132  			return nil, 0, err
   133  		}
   134  
   135  		// Obtain fee estimates both with and without using a change
   136  		// output.
   137  		requiredFeeNoChange, requiredFeeWithChange, err := calculateFees(
   138  			selectedUtxos, feeRate,
   139  		)
   140  		if err != nil {
   141  			return nil, 0, err
   142  		}
   143  
   144  		// The difference between the selected amount and the amount
   145  		// requested will be used to pay fees, and generate a change
   146  		// output with the remaining.
   147  		overShootAmt := totalAtoms - amt
   148  
   149  		var changeAmt dcrutil.Amount
   150  
   151  		switch {
   152  
   153  		// If the excess amount isn't enough to pay for fees based on
   154  		// fee rate and estimated size without using a change output,
   155  		// then increase the requested coin amount by the estimate
   156  		// required fee without using change, performing another round
   157  		// of coin selection.
   158  		case overShootAmt < requiredFeeNoChange:
   159  			amtNeeded = amt + requiredFeeNoChange
   160  			continue
   161  
   162  		// If sufficient funds were selected to cover the fee required
   163  		// to include a change output, the remainder will be our change
   164  		// amount.
   165  		case overShootAmt > requiredFeeWithChange:
   166  			changeAmt = overShootAmt - requiredFeeWithChange
   167  
   168  		// Otherwise we have selected enough to pay for a tx without a
   169  		// change output.
   170  		default:
   171  			changeAmt = 0
   172  
   173  		}
   174  
   175  		if changeAmt < dustLimit {
   176  			changeAmt = 0
   177  		}
   178  
   179  		// Sanity check the resulting output values to make sure we
   180  		// don't burn a great part to fees.
   181  		totalOut := amt + changeAmt
   182  		err = sanityCheckFee(totalOut, totalAtoms-totalOut)
   183  		if err != nil {
   184  			return nil, 0, err
   185  		}
   186  
   187  		return selectedUtxos, changeAmt, nil
   188  	}
   189  }
   190  
   191  // CoinSelectSubtractFees attempts to select coins such that we'll spend up to
   192  // amt in total after fees, adhering to the specified fee rate. The selected
   193  // coins, the final output and change values are returned.
   194  func CoinSelectSubtractFees(feeRate chainfee.AtomPerKByte, amt,
   195  	dustLimit dcrutil.Amount, coins []Coin) ([]Coin, dcrutil.Amount,
   196  	dcrutil.Amount, error) {
   197  
   198  	// First perform an initial round of coin selection to estimate
   199  	// the required fee.
   200  	totalAtoms, selectedUtxos, err := selectInputs(amt, coins)
   201  	if err != nil {
   202  		return nil, 0, 0, err
   203  	}
   204  
   205  	// Obtain fee estimates both with and without using a change
   206  	// output.
   207  	requiredFeeNoChange, requiredFeeWithChange, err := calculateFees(
   208  		selectedUtxos, feeRate)
   209  	if err != nil {
   210  		return nil, 0, 0, err
   211  	}
   212  
   213  	// For a transaction without a change output, we'll let everything go
   214  	// to our multi-sig output after subtracting fees.
   215  	outputAmt := totalAtoms - requiredFeeNoChange
   216  	changeAmt := dcrutil.Amount(0)
   217  
   218  	// If the the output is too small after subtracting the fee, the coin
   219  	// selection cannot be performed with an amount this small.
   220  	if outputAmt < dustLimit {
   221  		return nil, 0, 0, fmt.Errorf("output amount(%v) after "+
   222  			"subtracting fees(%v) below dust limit(%v)", outputAmt,
   223  			requiredFeeNoChange, dustLimit)
   224  	}
   225  
   226  	// For a transaction with a change output, everything we don't spend
   227  	// will go to change.
   228  	newOutput := amt - requiredFeeWithChange
   229  	newChange := totalAtoms - amt
   230  
   231  	// If adding a change output leads to both outputs being above
   232  	// the dust limit, we'll add the change output. Otherwise we'll
   233  	// go with the no change tx we originally found.
   234  	if newChange >= dustLimit && newOutput >= dustLimit {
   235  		outputAmt = newOutput
   236  		changeAmt = newChange
   237  	}
   238  
   239  	// Sanity check the resulting output values to make sure we
   240  	// don't burn a great part to fees.
   241  	totalOut := outputAmt + changeAmt
   242  	err = sanityCheckFee(totalOut, totalAtoms-totalOut)
   243  	if err != nil {
   244  		return nil, 0, 0, err
   245  	}
   246  
   247  	return selectedUtxos, outputAmt, changeAmt, nil
   248  }