lnwallet: replace naive coin selection a size+fee aware version

This commit is contained in:
Olaoluwa Osuntokun 2016-08-12 15:35:33 -07:00
parent 6a1d8d0682
commit 773c831561
No known key found for this signature in database
GPG Key ID: 9CC5B105D03521A2

@ -502,7 +502,9 @@ func (l *LightningWallet) handleFundingReserveRequest(req *initFundingReserveMsg
// don't need to perform any coin selection. Otherwise, attempt to // don't need to perform any coin selection. Otherwise, attempt to
// obtain enough coins to meet the required funding amount. // obtain enough coins to meet the required funding amount.
if req.fundingAmount != 0 { if req.fundingAmount != 0 {
if err := l.selectCoinsAndChange(req.fundingAmount, // TODO(roasbeef): consult model for proper fee rate
feeRate := uint64(10)
if err := l.selectCoinsAndChange(feeRate, req.fundingAmount,
ourContribution); err != nil { ourContribution); err != nil {
req.err <- err req.err <- err
req.resp <- nil req.resp <- nil
@ -1327,7 +1329,7 @@ func (l *LightningWallet) ListUnspentWitness(minConfs int32) ([]*btcjson.ListUns
// the passed contribution's inputs. If necessary, a change address will also be // the passed contribution's inputs. If necessary, a change address will also be
// generated. // generated.
// TODO(roasbeef): remove hardcoded fees and req'd confs for outputs. // TODO(roasbeef): remove hardcoded fees and req'd confs for outputs.
func (l *LightningWallet) selectCoinsAndChange(numCoins btcutil.Amount, func (l *LightningWallet) selectCoinsAndChange(feeRate uint64, amt btcutil.Amount,
contribution *ChannelContribution) error { contribution *ChannelContribution) error {
// We hold the coin select mutex while querying for outputs, and // We hold the coin select mutex while querying for outputs, and
@ -1337,22 +1339,10 @@ func (l *LightningWallet) selectCoinsAndChange(numCoins btcutil.Amount,
// when we encounter an error condition. // when we encounter an error condition.
l.coinSelectMtx.Lock() l.coinSelectMtx.Lock()
// TODO(roasbeef): check if balance is insufficient, if so then select
// on two channels, one is a time.After that will bail out with
// insuffcient funds, the other is a notification that the balance has
// been updated make(chan struct{}, 1).
// Find all unlocked unspent witness outputs with greater than 1 // Find all unlocked unspent witness outputs with greater than 1
// confirmation. // confirmation.
// TODO(roasbeef): make num confs a configuration paramter // TODO(roasbeef): make num confs a configuration paramter
unspentOutputs, err := l.ListUnspentWitness(1) coins, err := l.ListUnspentWitness(1)
if err != nil {
l.coinSelectMtx.Unlock()
return err
}
// Convert the outputs to coins for coin selection below.
coins, err := outputsToCoins(unspentOutputs)
if err != nil { if err != nil {
l.coinSelectMtx.Unlock() l.coinSelectMtx.Unlock()
return err return err
@ -1360,17 +1350,8 @@ func (l *LightningWallet) selectCoinsAndChange(numCoins btcutil.Amount,
// Peform coin selection over our available, unlocked unspent outputs // Peform coin selection over our available, unlocked unspent outputs
// in order to find enough coins to meet the funding amount requirements. // in order to find enough coins to meet the funding amount requirements.
// // TODO(roasbeef): take in sat/byte
// TODO(roasbeef): Should extend coinset with optimal coin selection selectedCoins, changeAmt, err := coinSelect(feeRate, amt, coins)
// heuristics for our use case.
// NOTE: this current selection assumes "priority" is still a thing.
selector := &coinset.MaxValueAgeCoinSelector{
MaxInputs: 10,
MinChangeAmount: 10000,
}
// TODO(roasbeef): don't hardcode fee...
totalWithFee := numCoins + 10000
selectedCoins, err := selector.CoinSelect(totalWithFee, coins)
if err != nil { if err != nil {
l.coinSelectMtx.Unlock() l.coinSelectMtx.Unlock()
return err return err
@ -1379,42 +1360,37 @@ func (l *LightningWallet) selectCoinsAndChange(numCoins btcutil.Amount,
// Lock the selected coins. These coins are now "reserved", this // Lock the selected coins. These coins are now "reserved", this
// prevents concurrent funding requests from referring to and this // prevents concurrent funding requests from referring to and this
// double-spending the same set of coins. // double-spending the same set of coins.
contribution.Inputs = make([]*wire.TxIn, len(selectedCoins.Coins())) contribution.Inputs = make([]*wire.TxIn, len(selectedCoins))
for i, coin := range selectedCoins.Coins() { for i, coin := range selectedCoins {
txout := wire.NewOutPoint(coin.Hash(), coin.Index()) l.lockedOutPoints[*coin] = struct{}{}
l.LockOutpoint(*txout) l.LockOutpoint(*coin)
// Empty sig script, we'll actually sign if this reservation is // Empty sig script, we'll actually sign if this reservation is
// queued up to be completed (the other side accepts). // queued up to be completed (the other side accepts).
outPoint := wire.NewOutPoint(coin.Hash(), coin.Index()) contribution.Inputs[i] = wire.NewTxIn(coin, nil, nil)
contribution.Inputs[i] = wire.NewTxIn(outPoint, nil, nil) }
// Record any change output(s) generated as a result of the coin
// selection.
if changeAmt != 0 {
changeAddr, err := l.NewAddress(WitnessPubKey, true)
if err != nil {
return err
}
changeScript, err := txscript.PayToAddrScript(changeAddr)
if err != nil {
return err
}
contribution.ChangeOutputs = make([]*wire.TxOut, 1)
contribution.ChangeOutputs[0] = &wire.TxOut{
Value: int64(changeAmt),
PkScript: changeScript,
}
} }
l.coinSelectMtx.Unlock() l.coinSelectMtx.Unlock()
// Create some possibly neccessary change outputs.
selectedTotalValue := coinset.NewCoinSet(selectedCoins.Coins()).TotalValue()
if selectedTotalValue > totalWithFee {
// Change is necessary. Query for an available change address to
// send the remainder to.
contribution.ChangeOutputs = make([]*wire.TxOut, 1)
changeAddr, err := l.NewChangeAddress(waddrmgr.DefaultAccountNum,
waddrmgr.WitnessPubKey)
if err != nil {
return err
}
changeAddrScript, err := txscript.PayToAddrScript(changeAddr)
if err != nil {
return err
}
changeAmount := selectedTotalValue - totalWithFee
contribution.ChangeOutputs[0] = wire.NewTxOut(int64(changeAmount),
changeAddrScript)
}
// TODO(roasbeef): re-calculate fees here to minFeePerKB, may need more inputs
return nil return nil
} }
@ -1428,8 +1404,107 @@ func (w *WaddrmgrEncryptorDecryptor) Encrypt(p []byte) ([]byte, error) {
func (w *WaddrmgrEncryptorDecryptor) Decrypt(c []byte) ([]byte, error) { func (w *WaddrmgrEncryptorDecryptor) Decrypt(c []byte) ([]byte, error) {
return w.M.Decrypt(waddrmgr.CKTPrivate, c) return w.M.Decrypt(waddrmgr.CKTPrivate, c)
// selectInputs selects a slice of inputs necessary to meet the specified
// selection amount. If input selectino is unable to suceed to to insuffcient
// 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, []*wire.OutPoint, error) {
var (
selectedUtxos []*wire.OutPoint
satSelected btcutil.Amount
)
i := 0
for satSelected < amt {
// If we're about to go past the number of available coins,
// then exit with an error.
if i > len(coins)-1 {
return 0, nil, ErrInsufficientFunds
}
// Otherwise, collect this new coin as it may be used for final
// coin selection.
coin := coins[i]
utxo := &wire.OutPoint{
Hash: coin.Hash,
Index: coin.Index,
}
selectedUtxos = append(selectedUtxos, utxo)
satSelected += coin.Value
i++
}
return satSelected, selectedUtxos, nil
} }
func (w *WaddrmgrEncryptorDecryptor) OverheadSize() uint32 { func (w *WaddrmgrEncryptorDecryptor) OverheadSize() uint32 {
return 24 return 24
// coinSelect attemps to select a sufficient amount of coins, including a
// change output to fund amt satoshis, adhearing to the specified fee rate. The
// specified fee rate should be expressed in sat/byte for coin selection to
// function properly.
func coinSelect(feeRate uint64, amt btcutil.Amount,
coins []*Utxo) ([]*wire.OutPoint, btcutil.Amount, error) {
const (
// txOverhead is the overhead of a transaction residing within
// the version number and lock time.
txOverhead = 8
// p2wkhSpendSize an estimate of the number of bytes it takes
// to spend a p2wkh output.
//
// (p2wkh witness) + txid + index + varint script size + sequence
// TODO(roasbeef): div by 3 due to witness size?
p2wkhSpendSize = (1 + 73 + 1 + 33) + 32 + 4 + 1 + 4
// p2wkhOutputSize is an estimate of the size of a regualr
// p2wkh output.
//
// 8 (output) + 1 (var int script) + 22 (p2wkh output)
p2wkhOutputSize = 8 + 1 + 22
// p2wkhOutputSize is an estimate of the p2wsh funding uotput.
p2wshOutputSize = 8 + 1 + 34
)
var estimatedSize int
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
}
// Based on the selected coins, estimate the size of the final
// fully signed transaction.
estimatedSize = ((len(selectedUtxos) * p2wkhSpendSize) +
p2wshOutputSize + txOverhead)
// The difference bteween the selected amount and the amount
// requested will be used to pay fees, and generate a change
// output with the remaining.
overShootAmt := totalSat - amtNeeded
// 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.
requiredFee := btcutil.Amount(uint64(estimatedSize) * feeRate)
if overShootAmt < requiredFee {
amtNeeded += requiredFee
continue
}
// If the fee is sufficient, then calculate the size of the change output.
changeAmt := overShootAmt - requiredFee
return selectedUtxos, changeAmt, nil
}
} }