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 }