From a2dcca2b082c5595448cdc05da9ce19fdf0a7387 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 7 Dec 2018 08:36:58 +0100 Subject: [PATCH] sweep: add input partitionings generator This commit adds a function that takes a set of inputs and splits them in sensible sets to be used for generating transactions. --- sweep/txgenerator.go | 152 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/sweep/txgenerator.go b/sweep/txgenerator.go index 7adbdf85..e173706a 100644 --- a/sweep/txgenerator.go +++ b/sweep/txgenerator.go @@ -2,13 +2,165 @@ package sweep import ( "fmt" + "sort" + "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/wallet/txrules" "github.com/lightningnetwork/lnd/lnwallet" ) +var ( + // DefaultMaxInputsPerTx specifies the default maximum number of inputs + // allowed in a single sweep tx. If more need to be swept, multiple txes + // are created and published. + DefaultMaxInputsPerTx = 100 +) + +// inputSet is a set of inputs that can be used as the basis to generate a tx +// on. +type inputSet []Input + +// generateInputPartitionings goes through all given inputs and constructs sets +// of inputs that can be used to generate a sensible transaction. Each set +// contains up to the configured maximum number of inputs. Negative yield +// inputs are skipped. No input sets with a total value after fees below the +// dust limit are returned. +func generateInputPartitionings(sweepableInputs []Input, + relayFeePerKW, feePerKW lnwallet.SatPerKWeight, + maxInputsPerTx int) ([]inputSet, error) { + + // Calculate dust limit based on the P2WPKH output script of the sweep + // txes. + dustLimit := txrules.GetDustThreshold( + lnwallet.P2WPKHSize, + btcutil.Amount(relayFeePerKW.FeePerKVByte()), + ) + + // Sort input by yield. We will start constructing input sets starting + // with the highest yield inputs. This is to prevent the construction + // of a set with an output below the dust limit, causing the sweep + // process to stop, while there are still higher value inputs + // available. It also allows us to stop evaluating more inputs when the + // first input in this ordering is encountered with a negative yield. + // + // Yield is calculated as the difference between value and added fee + // for this input. The fee calculation excludes fee components that are + // common to all inputs, as those wouldn't influence the order. The + // single component that is differentiating is witness size. + // + // For witness size, the upper limit is taken. The actual size depends + // on the signature length, which is not known yet at this point. + yields := make(map[wire.OutPoint]int64) + for _, input := range sweepableInputs { + size, err := getInputWitnessSizeUpperBound(input) + if err != nil { + return nil, fmt.Errorf( + "failed adding input weight: %v", err) + } + + yields[*input.OutPoint()] = input.SignDesc().Output.Value - + int64(feePerKW.FeeForWeight(int64(size))) + } + + sort.Slice(sweepableInputs, func(i, j int) bool { + return yields[*sweepableInputs[i].OutPoint()] > + yields[*sweepableInputs[j].OutPoint()] + }) + + // Select blocks of inputs up to the configured maximum number. + var sets []inputSet + for len(sweepableInputs) > 0 { + // Get the maximum number of inputs from sweepableInputs that + // we can use to create a positive yielding set from. + count, outputValue := getPositiveYieldInputs( + sweepableInputs, maxInputsPerTx, feePerKW, + ) + + // If there are no positive yield inputs left, we can stop + // here. + if count == 0 { + return sets, nil + } + + // If the output value of this block of inputs does not reach + // the dust limit, stop sweeping. Because of the sorting, + // continuing with the remaining inputs will only lead to sets + // with a even lower output value. + if outputValue < dustLimit { + log.Debugf("Set value %v below dust limit of %v", + outputValue, dustLimit) + return sets, nil + } + + log.Infof("Candidate sweep set of size=%v, has yield=%v", + count, outputValue) + + sets = append(sets, sweepableInputs[:count]) + sweepableInputs = sweepableInputs[count:] + } + + return sets, nil +} + +// getPositiveYieldInputs returns the maximum of a number n for which holds +// that the inputs [0,n) of sweepableInputs have a positive yield. +// Additionally, the total values of these inputs minus the fee is returned. +// +// TODO(roasbeef): Consider including some negative yield inputs too to clean +// up the utxo set even if it costs us some fees up front. In the spirit of +// minimizing any negative externalities we cause for the Bitcoin system as a +// whole. +func getPositiveYieldInputs(sweepableInputs []Input, maxInputs int, + feePerKW lnwallet.SatPerKWeight) (int, btcutil.Amount) { + + var weightEstimate lnwallet.TxWeightEstimator + + // Add the sweep tx output to the weight estimate. + weightEstimate.AddP2WKHOutput() + + var total, outputValue btcutil.Amount + for idx, input := range sweepableInputs { + // Can ignore error, because it has already been checked when + // calculating the yields. + size, _ := getInputWitnessSizeUpperBound(input) + + // Keep a running weight estimate of the input set. + weightEstimate.AddWitnessInput(size) + + newTotal := total + btcutil.Amount(input.SignDesc().Output.Value) + + weight := weightEstimate.Weight() + fee := feePerKW.FeeForWeight(int64(weight)) + + // Calculate the output value if the current input would be + // added to the set. + newOutputValue := newTotal - fee + + // If adding this input makes the total output value of the set + // decrease, this is a negative yield input. It shouldn't be + // added to the set. We return the current index as the number + // of inputs, so the current input is being excluded. + if newOutputValue <= outputValue { + return idx, outputValue + } + + // Update running values. + total = newTotal + outputValue = newOutputValue + + // Stop if max inputs is reached. + if idx == maxInputs-1 { + return maxInputs, outputValue + } + } + + // We could add all inputs to the set, so return them all. + return len(sweepableInputs), outputValue +} + // createSweepTx builds a signed tx spending the inputs to a the output script. func createSweepTx(inputs []Input, outputPkScript []byte, currentBlockHeight uint32, feePerKw lnwallet.SatPerKWeight,