diff --git a/contractcourt/anchor_resolver.go b/contractcourt/anchor_resolver.go index ec90b6ed3..b4d687720 100644 --- a/contractcourt/anchor_resolver.go +++ b/contractcourt/anchor_resolver.go @@ -99,8 +99,6 @@ func (c *anchorResolver) Resolve(_ bool) (ContractResolver, error) { // After a restart or when the remote force closes, the sweeper is not // yet aware of the anchor. In that case, it will be added as new input // to the sweeper. - relayFeeRate := c.Sweeper.RelayFeePerKW() - witnessType := input.CommitmentAnchor // For taproot channels, we need to use the proper witness type. @@ -116,9 +114,6 @@ func (c *anchorResolver) Resolve(_ bool) (ContractResolver, error) { resultChan, err := c.Sweeper.SweepInput( &anchorInput, sweep.Params{ - Fee: sweep.FeeEstimateInfo{ - FeeRate: relayFeeRate, - }, // For normal anchor sweeping, the budget is 330 sats. Budget: btcutil.Amount( anchorInput.SignDesc().Output.Value, diff --git a/contractcourt/commit_sweep_resolver_test.go b/contractcourt/commit_sweep_resolver_test.go index bf6f70cbc..f2b43b0f8 100644 --- a/contractcourt/commit_sweep_resolver_test.go +++ b/contractcourt/commit_sweep_resolver_test.go @@ -1,7 +1,6 @@ package contractcourt import ( - "fmt" "testing" "time" @@ -135,20 +134,6 @@ func (s *mockSweeper) SweepInput(input input.Input, params sweep.Params) ( s.sweptInputs <- input - // TODO(yy): replace mockSweeper with `mock.Mock`. - if params.Fee != nil { - fee, ok := params.Fee.(sweep.FeeEstimateInfo) - if !ok { - return nil, fmt.Errorf("unexpected fee type: %T", - params.Fee) - } - - // Update the deadlines used if it's set. - if fee.ConfTarget != 0 { - s.deadlines = append(s.deadlines, int(fee.ConfTarget)) - } - } - // Update the deadlines used if it's set. params.DeadlineHeight.WhenSome(func(d int32) { s.deadlines = append(s.deadlines, int(d)) diff --git a/docs/release-notes/release-notes-0.18.0.md b/docs/release-notes/release-notes-0.18.0.md index 18969e6c4..955905354 100644 --- a/docs/release-notes/release-notes-0.18.0.md +++ b/docs/release-notes/release-notes-0.18.0.md @@ -204,7 +204,9 @@ bitcoin peers' feefilter values into account](https://github.com/lightningnetwor by default, and the feature is not yet advertised to the network. * Introduced [fee bumper](https://github.com/lightningnetwork/lnd/pull/8424) to - handle bumping the fees of sweeping transactions properly. + handle bumping the fees of sweeping transactions properly. A + [README.md](https://github.com/lightningnetwork/lnd/pull/8674) is added to + explain this new approach. ## RPC Additions diff --git a/sweep/README.md b/sweep/README.md new file mode 100644 index 000000000..487ebdcdd --- /dev/null +++ b/sweep/README.md @@ -0,0 +1,213 @@ +# Sweep + +`sweep` is a subservice that handles sweeping UTXOs back to `lnd`'s wallet. Its +main purpose is to sweep back the outputs resulting from a force close +transaction, although users can also call `BumpFee` to feed new unconfirmed +inputs to be handled by the sweeper. + +In order to sweep economically, the sweeper needs to understand the time +sensitivity and max fees that can be used when sweeping the inputs. This means +each input must come with a deadline and a fee budget, which can be set via the +RPC request or the config, otherwise the default values will be used. Once +offered to the sweeper, when a new block arrives, inputs with the same deadline +will be batched into a single sweeping transaction to minimize the cost. + +The sweeper will publish this transaction and monitor it for potential fee +bumping, a process that won’t exit until the sweeping transaction is confirmed, +or the specified budget has been used up. + +## Understanding Budget and Deadline + +There are two questions when spending a UTXO - how much fees to pay and what +the confirmation target is, which gives us the concepts of budget and deadline. +This is especially important when sweeping the outputs of a force close +transaction - some of the outputs are time-sensitive, and may result in fund +loss if not confirmed in time. On the other hand, we don’t want to pay more +than what we can get back - if a sweeping transaction spends more than what is +meant to be swept, we are losing money due to fees. + +To properly handle the case, the concept `budget` and `deadline` have been +introduced to `lnd` since `v0.18.0` - for each new sweeping request, the +sweeper requires the caller to specify a deadline and a budget so it can make +economic decisions. A fee function is then created based on the budget and +deadline, which proposes a fee rate to use for the sweeping transaction. When a +new block arrives, unless the transaction is confirmed or the budget is used +up, the sweeper will perform a fee bump on it via RBF. + +## Package Structure + +On a high level, a UTXO is offered to the sweeper via `SweepInput`. The sweeper +keeps track of the pending inputs. When a new block arrives, it asks the +`UtxoAggregator` to group all the pending inputs into batches via +`ClusterInputs`. Each batch is an `InputSet`, and is sent to the `Bumper`. The +`Bumper` creates a `FeeFunction` and a sweeping transaction using the +`InputSet`, and monitors its confirmation status. Every time it's not confirmed +when a new block arrives, the `Bumper` will perform an RBF by calling +`IncreaseFeeRate` on the `FeeFunction`. + +```mermaid +flowchart LR + subgraph SweepInput + UTXO1-->sweeper + UTXO2-->sweeper + UTXO3-->sweeper + UTXO["..."]-->sweeper + sweeper + end + + subgraph ClusterInputs + sweeper-->UtxoAggregator + UtxoAggregator-->InputSet1 + UtxoAggregator-->InputSet2 + UtxoAggregator-->InputSet["..."] + end + + subgraph Broadcast + InputSet1-->Bumper + InputSet2-->Bumper + InputSet-->Bumper + end + + subgraph IncreaseFeeRate + FeeFunction-->Bumper + end + + block["new block"] ==> ClusterInputs +``` + +#### `UtxoAggregator` and `InputSet` + +`UtxoAggregator` is an interface that handles the batching of inputs. +`BudgetAggregator` implements this interface by grouping inputs with the same +deadline together. Inputs with the same deadline express the same time +sensitivity so it makes sense to sweep them in the same transaction. Once +grouped, inputs in each batch are sorted based on their budgets. The only +exception is inputs with `ExclusiveGroup` flag set, which will be swept alone. + +Once the batching is finished, an `InputSet` is returned, which is an interface +used to decide whether a wallet UTXO is needed or not when creating the +sweeping transaction. `BudgetInputSet` implements this interface by checking +the sum of the output values from these inputs against the sum of their +budgets - if the total budget cannot be covered, one or more wallet UTXOs are +needed. + +For instance, when anchor output is swept to perform a CPFP, one or more wallet +UTXOs are likely to be used to meet the specified budget, which is also the +case when sweeping second-level HTLC transactions. However, if the sweeping +transaction also contains other to-be-swept inputs, a wallet UTXO is no longer +needed if their values can cover the total budget. + +#### `Bumper` + +`Bumper` is a transaction creator, publisher, and monitor that works on an +`InputSet`. Once a sweeping transaction is created using the `InputSet`, the +`Bumper` will monitor its confirmation status and attempt an RBF if the +transaction is not confirmed in the next block. It relies on the `FeeFunction` +to determine the new fee rate every block, and this new fee rate may or may not +meet the BIP 125 fee requirements - in that case, the `Bumper` will try to +perform an RBF again in the coming blocks. + +`TxPublisher` implements the `Bumper` interface. When a transaction is created +for the first time, unless its budget has been used up, `TxPublisher` will +guarantee that the initial publish meets the RBF requirements. + +#### `FeeFunction` + +`FeeFunction` is an interface that specifies a function over a starting fee +rate, an ending fee rate, and a width (the deadline delta). It's used by the +`Bumper` to suggest a new fee rate for bumping the sweeping transaction. + +`LinearFeeFunction` implements this interface using a linear function - it +calculates a fee rate delta using `(ending_fee_rate - starting_fee_rate) / +deadline`, and increases the fee rate by this delta value everytime a new block +arrives. Once the deadline is passed, `LinearFeeFunction` will cap its +returning fee rate at the ending fee rate. + +The starting fee rate is the estimated fee rate from the fee estimator, which +is the result from calling `estimatesmartfee`(`bitcoind`), +`estimatefee`(`btcd`), or `feeurl` depending on the config. This fee estimator +is called using the deadline as the conf target, and the returned fee rate is +used as the starting fee rate. This behavior can be overridden by setting the +`--sat_per_vbyte` via `bumpfee` cli when fee bumping a specific input, which +allows users to bypass the fee estimation and set the starting fee rate +directly. + +The ending fee rate is the value from dividing the budget by the size of the +sweeping transaction, and capped at the `--sweeper.maxfeerate`. The ending fee +rate can be overridden by setting the `--budget` via `bumpfee` cli. + +For instance, suppose `lnd` is using `bitcoind` as its fee estimator, and an +input with a deadline of 1000 blocks and a budget of 200,000 sats is being +swept in a transaction that has a size of 500 vbytes, the fee function will be +initialized with: + +- a starting fee rate of 10 sat/vB, which is the result from calling + `estimatesmartfee 1000`. +- an ending fee rate of 400 sat/vB, which is the result of `200,000/500`. +- a fee rate delta of 390 sat/kvB, which is the result of `(400 - 10) / 500 * + 1000`. + +## Sweeping Outputs from a Force Close Transaction + +A force close transaction may have the following outputs: + +- Commit outputs, which are the `to_local` and `to_remote` outputs. +- HTLC outputs, which are the `incoming_htlc` and `outgoing_htlc` outputs. +- Anchor outputs, which are the local and remote anchor outputs. + +#### Sweeping Commit Outputs + +The only output we can spend is the `to_local` output. Because it can only be +spent using our signature, there’s no time pressure here. By default, the +sweeper will use a deadline of 1008 blocks as the confirmation target for +non-time-sensitive outputs. To overwrite the default, users can specify a +value using the config `--sweeper.nodeadlineconftarget`. + +To specify the budget, users can use `--sweeper.budget.tolocal` to set the max +allowed fees in sats, or use `--sweeper.budget.tolocalratio` to set a +proportion of the `to_local` value to be used as the budget. + +#### Sweeping HTLC Outputs + +When facing a local force close transaction, HTLCs are spent in a two-stage +setup - the first stage is to spend the outputs using pre-signed HTLC +success/timeout transactions, the second stage is to spend the outputs from +these success/timeout transactions. All these outputs are automatically handled +by `lnd`. In specific, +- For an incoming HTLC in stage one, the deadline is specified using its CLTV + from the timeout path. This output is time-sensitive. +- For an outgoing HTLC in stage one, the deadline is derived from its + corresponding incoming HTLC’s CLTV. This output is time-sensitive. +- For both incoming and outgoing HTLCs in stage two, because they can only be + spent by us, there is no time pressure to confirm them under a deadline. + +When facing a remote force close transaction, HTLCs can be directly spent from +the commitment transaction, and both incoming and outgoing HTLCs are +time-sensitive. + +By default, `lnd` will use 50% of the HTLC value as its budget. To customize +it, users can specify `--sweeper.budget.deadlinehtlc` and +`--sweeper.budget.deadlinehtlcratio` for time-sensitive HTLCs, and +`--sweeper.budget.nodeadlinehtlc` and `--sweeper.budget.nodeadlinehtlcratio` +for non-time-sensitive sweeps. + +#### Sweeping Anchor Outputs + +An anchor output is a special output that functions as “anchor” to speed up the +unconfirmed force closing transaction via CPFP. If the force close transaction +doesn't contain any HTLCs, the anchor output is generally uneconomical to sweep +and will be ignored. However, if the force close transaction does contain +time-sensitive outputs (HTLCs), the anchor output will be swept to CPFP the +transaction and accelerate the force close process. + +For CPFP-purpose anchor sweeping, the deadline is the closest deadline value of +all the HTLCs on the force close transaction. The budget, however, cannot be a +ratio of the anchor output because the value is too small to contribute +meaningful fees (330 sats). Since its purpose is to accelerate the force close +transaction so the time-sensitive outputs can be swept, the budget is actually +drawn from what we call “value under protection”, which is the sum of all HTLC +outputs minus the sum of their budgets. By default, 50% of this value is used +as the budget, to customize it, either use +`--sweeper.budget.anchorcpfp` to specify sats, or use +`--sweeper.budget.anchorcpfpratio` to specify a ratio. + diff --git a/sweep/aggregator.go b/sweep/aggregator.go index a65fa39c2..c812cbe2b 100644 --- a/sweep/aggregator.go +++ b/sweep/aggregator.go @@ -9,113 +9,6 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) -const ( - // DefaultFeeRateBucketSize is the default size of fee rate buckets - // we'll use when clustering inputs into buckets with similar fee rates - // within the SimpleAggregator. - // - // Given a minimum relay fee rate of 1 sat/vbyte, a multiplier of 10 - // would result in the following fee rate buckets up to the maximum fee - // rate: - // - // #1: min = 1 sat/vbyte, max = 10 sat/vbyte - // #2: min = 11 sat/vbyte, max = 20 sat/vbyte... - DefaultFeeRateBucketSize = 10 -) - -// inputCluster is a helper struct to gather a set of pending inputs that -// should be swept with the specified fee rate. -type inputCluster struct { - lockTime *uint32 - sweepFeeRate chainfee.SatPerKWeight - inputs InputsMap -} - -// createInputSets goes through the cluster's inputs and constructs sets of -// inputs that can be used to generate a sweeping 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 (c *inputCluster) createInputSets(maxFeeRate chainfee.SatPerKWeight, - maxInputs uint32) []InputSet { - - // Turn the inputs into a slice so we can sort them. - inputList := make([]*SweeperInput, 0, len(c.inputs)) - for _, input := range c.inputs { - inputList = append(inputList, input) - } - - // 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. - calcYield := func(input *SweeperInput) int64 { - size, _, err := input.WitnessType().SizeUpperBound() - if err != nil { - log.Errorf("Failed to get input weight: %v", err) - - return 0 - } - - yield := input.SignDesc().Output.Value - - int64(c.sweepFeeRate.FeeForWeight(int64(size))) - - return yield - } - - // 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. - sort.Slice(inputList, func(i, j int) bool { - // Because of the specific ordering and termination condition - // that is described above, we place force sweeps at the start - // of the list. Otherwise we can't be sure that they will be - // included in an input set. - if inputList[i].parameters().Immediate { - return true - } - - return calcYield(inputList[i]) > calcYield(inputList[j]) - }) - - // Select blocks of inputs up to the configured maximum number. - var sets []InputSet - for len(inputList) > 0 { - // Start building a set of positive-yield tx inputs under the - // condition that the tx will be published with the specified - // fee rate. - txInputs := newTxInputSet(c.sweepFeeRate, maxFeeRate, maxInputs) - - // From the set of sweepable inputs, keep adding inputs to the - // input set until the tx output value no longer goes up or the - // maximum number of inputs is reached. - txInputs.addPositiveYieldInputs(inputList) - - // If there are no positive yield inputs, we can stop here. - inputCount := len(txInputs.inputs) - if inputCount == 0 { - return sets - } - - log.Infof("Candidate sweep set of size=%v (+%v wallet inputs),"+ - " has yield=%v, weight=%v", - inputCount, len(txInputs.inputs)-inputCount, - txInputs.totalOutput()-txInputs.walletInputTotal, - txInputs.weightEstimate(true).weight()) - - sets = append(sets, txInputs) - inputList = inputList[inputCount:] - } - - return sets -} - // UtxoAggregator defines an interface that takes a list of inputs and // aggregate them into groups. Each group is used as the inputs to create a // sweeping transaction. @@ -125,345 +18,6 @@ type UtxoAggregator interface { ClusterInputs(inputs InputsMap) []InputSet } -// SimpleAggregator aggregates inputs known by the Sweeper based on each -// input's locktime and feerate. -type SimpleAggregator struct { - // FeeEstimator is used when crafting sweep transactions to estimate - // the necessary fee relative to the expected size of the sweep - // transaction. - FeeEstimator chainfee.Estimator - - // MaxFeeRate is the maximum fee rate allowed within the - // SimpleAggregator. - MaxFeeRate chainfee.SatPerKWeight - - // MaxInputsPerTx 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. - MaxInputsPerTx uint32 - - // FeeRateBucketSize is the default size of fee rate buckets we'll use - // when clustering inputs into buckets with similar fee rates within - // the SimpleAggregator. - // - // Given a minimum relay fee rate of 1 sat/vbyte, a fee rate bucket - // size of 10 would result in the following fee rate buckets up to the - // maximum fee rate: - // - // #1: min = 1 sat/vbyte, max (exclusive) = 11 sat/vbyte - // #2: min = 11 sat/vbyte, max (exclusive) = 21 sat/vbyte... - FeeRateBucketSize int -} - -// Compile-time constraint to ensure SimpleAggregator implements UtxoAggregator. -var _ UtxoAggregator = (*SimpleAggregator)(nil) - -// NewSimpleUtxoAggregator creates a new instance of a SimpleAggregator. -func NewSimpleUtxoAggregator(estimator chainfee.Estimator, - max chainfee.SatPerKWeight, maxTx uint32) *SimpleAggregator { - - return &SimpleAggregator{ - FeeEstimator: estimator, - MaxFeeRate: max, - MaxInputsPerTx: maxTx, - FeeRateBucketSize: DefaultFeeRateBucketSize, - } -} - -// ClusterInputs creates a list of input clusters from the set of pending -// inputs known by the UtxoSweeper. It clusters inputs by -// 1) Required tx locktime -// 2) Similar fee rates. -func (s *SimpleAggregator) ClusterInputs(inputs InputsMap) []InputSet { - // We start by getting the inputs clusters by locktime. Since the - // inputs commit to the locktime, they can only be clustered together - // if the locktime is equal. - lockTimeClusters, nonLockTimeInputs := s.clusterByLockTime(inputs) - - // Cluster the remaining inputs by sweep fee rate. - feeClusters := s.clusterBySweepFeeRate(nonLockTimeInputs) - - // Since the inputs that we clustered by fee rate don't commit to a - // specific locktime, we can try to merge a locktime cluster with a fee - // cluster. - clusters := zipClusters(lockTimeClusters, feeClusters) - - sort.Slice(clusters, func(i, j int) bool { - return clusters[i].sweepFeeRate > - clusters[j].sweepFeeRate - }) - - // Now that we have the clusters, we can create the input sets. - var inputSets []InputSet - for _, cluster := range clusters { - sets := cluster.createInputSets( - s.MaxFeeRate, s.MaxInputsPerTx, - ) - inputSets = append(inputSets, sets...) - } - - return inputSets -} - -// clusterByLockTime takes the given set of pending inputs and clusters those -// with equal locktime together. Each cluster contains a sweep fee rate, which -// is determined by calculating the average fee rate of all inputs within that -// cluster. In addition to the created clusters, inputs that did not specify a -// required locktime are returned. -func (s *SimpleAggregator) clusterByLockTime( - inputs InputsMap) ([]inputCluster, InputsMap) { - - locktimes := make(map[uint32]InputsMap) - rem := make(InputsMap) - - // Go through all inputs and check if they require a certain locktime. - for op, input := range inputs { - lt, ok := input.RequiredLockTime() - if !ok { - rem[op] = input - continue - } - - // Check if we already have inputs with this locktime. - cluster, ok := locktimes[lt] - if !ok { - cluster = make(InputsMap) - } - - // Get the fee rate based on the fee preference. If an error is - // returned, we'll skip sweeping this input for this round of - // cluster creation and retry it when we create the clusters - // from the pending inputs again. - feeRate, err := input.params.Fee.Estimate( - s.FeeEstimator, s.MaxFeeRate, - ) - if err != nil { - log.Warnf("Skipping input %v: %v", op, err) - continue - } - - log.Debugf("Adding input %v to cluster with locktime=%v, "+ - "feeRate=%v", op, lt, feeRate) - - // Attach the fee rate to the input. - input.lastFeeRate = feeRate - - // Update the cluster about the updated input. - cluster[op] = input - locktimes[lt] = cluster - } - - // We'll then determine the sweep fee rate for each set of inputs by - // calculating the average fee rate of the inputs within each set. - inputClusters := make([]inputCluster, 0, len(locktimes)) - for lt, cluster := range locktimes { - lt := lt - - var sweepFeeRate chainfee.SatPerKWeight - for _, input := range cluster { - sweepFeeRate += input.lastFeeRate - } - - sweepFeeRate /= chainfee.SatPerKWeight(len(cluster)) - inputClusters = append(inputClusters, inputCluster{ - lockTime: <, - sweepFeeRate: sweepFeeRate, - inputs: cluster, - }) - } - - return inputClusters, rem -} - -// clusterBySweepFeeRate takes the set of pending inputs within the UtxoSweeper -// and clusters those together with similar fee rates. Each cluster contains a -// sweep fee rate, which is determined by calculating the average fee rate of -// all inputs within that cluster. -func (s *SimpleAggregator) clusterBySweepFeeRate( - inputs InputsMap) []inputCluster { - - bucketInputs := make(map[int]*bucketList) - inputFeeRates := make(map[wire.OutPoint]chainfee.SatPerKWeight) - - // First, we'll group together all inputs with similar fee rates. This - // is done by determining the fee rate bucket they should belong in. - for op, input := range inputs { - feeRate, err := input.params.Fee.Estimate( - s.FeeEstimator, s.MaxFeeRate, - ) - if err != nil { - log.Warnf("Skipping input %v: %v", op, err) - continue - } - - // Only try to sweep inputs with an unconfirmed parent if the - // current sweep fee rate exceeds the parent tx fee rate. This - // assumes that such inputs are offered to the sweeper solely - // for the purpose of anchoring down the parent tx using cpfp. - parentTx := input.UnconfParent() - if parentTx != nil { - parentFeeRate := - chainfee.SatPerKWeight(parentTx.Fee*1000) / - chainfee.SatPerKWeight(parentTx.Weight) - - if parentFeeRate >= feeRate { - log.Debugf("Skipping cpfp input %v: "+ - "fee_rate=%v, parent_fee_rate=%v", op, - feeRate, parentFeeRate) - - continue - } - } - - feeGroup := s.bucketForFeeRate(feeRate) - - // Create a bucket list for this fee rate if there isn't one - // yet. - buckets, ok := bucketInputs[feeGroup] - if !ok { - buckets = &bucketList{} - bucketInputs[feeGroup] = buckets - } - - // Request the bucket list to add this input. The bucket list - // will take into account exclusive group constraints. - buckets.add(input) - - input.lastFeeRate = feeRate - inputFeeRates[op] = feeRate - } - - // We'll then determine the sweep fee rate for each set of inputs by - // calculating the average fee rate of the inputs within each set. - inputClusters := make([]inputCluster, 0, len(bucketInputs)) - for _, buckets := range bucketInputs { - for _, inputs := range buckets.buckets { - var sweepFeeRate chainfee.SatPerKWeight - for op := range inputs { - sweepFeeRate += inputFeeRates[op] - } - sweepFeeRate /= chainfee.SatPerKWeight(len(inputs)) - inputClusters = append(inputClusters, inputCluster{ - sweepFeeRate: sweepFeeRate, - inputs: inputs, - }) - } - } - - return inputClusters -} - -// bucketForFeeReate determines the proper bucket for a fee rate. This is done -// in order to batch inputs with similar fee rates together. -func (s *SimpleAggregator) bucketForFeeRate( - feeRate chainfee.SatPerKWeight) int { - - relayFeeRate := s.FeeEstimator.RelayFeePerKW() - - // Create an isolated bucket for sweeps at the minimum fee rate. This - // is to prevent very small outputs (anchors) from becoming - // uneconomical if their fee rate would be averaged with higher fee - // rate inputs in a regular bucket. - if feeRate == relayFeeRate { - return 0 - } - - return 1 + int(feeRate-relayFeeRate)/s.FeeRateBucketSize -} - -// mergeClusters attempts to merge cluster a and b if they are compatible. The -// new cluster will have the locktime set if a or b had a locktime set, and a -// sweep fee rate that is the maximum of a and b's. If the two clusters are not -// compatible, they will be returned unchanged. -func mergeClusters(a, b inputCluster) []inputCluster { - newCluster := inputCluster{} - - switch { - // Incompatible locktimes, return the sets without merging them. - case a.lockTime != nil && b.lockTime != nil && - *a.lockTime != *b.lockTime: - - return []inputCluster{a, b} - - case a.lockTime != nil: - newCluster.lockTime = a.lockTime - - case b.lockTime != nil: - newCluster.lockTime = b.lockTime - } - - if a.sweepFeeRate > b.sweepFeeRate { - newCluster.sweepFeeRate = a.sweepFeeRate - } else { - newCluster.sweepFeeRate = b.sweepFeeRate - } - - newCluster.inputs = make(InputsMap) - - for op, in := range a.inputs { - newCluster.inputs[op] = in - } - - for op, in := range b.inputs { - newCluster.inputs[op] = in - } - - return []inputCluster{newCluster} -} - -// zipClusters merges pairwise clusters from as and bs such that cluster a from -// as is merged with a cluster from bs that has at least the fee rate of a. -// This to ensure we don't delay confirmation by decreasing the fee rate (the -// lock time inputs are typically second level HTLC transactions, that are time -// sensitive). -func zipClusters(as, bs []inputCluster) []inputCluster { - // Sort the clusters by decreasing fee rates. - sort.Slice(as, func(i, j int) bool { - return as[i].sweepFeeRate > - as[j].sweepFeeRate - }) - sort.Slice(bs, func(i, j int) bool { - return bs[i].sweepFeeRate > - bs[j].sweepFeeRate - }) - - var ( - finalClusters []inputCluster - j int - ) - - // Go through each cluster in as, and merge with the next one from bs - // if it has at least the fee rate needed. - for i := range as { - a := as[i] - - switch { - // If the fee rate for the next one from bs is at least a's, we - // merge. - case j < len(bs) && bs[j].sweepFeeRate >= a.sweepFeeRate: - merged := mergeClusters(a, bs[j]) - finalClusters = append(finalClusters, merged...) - - // Increment j for the next round. - j++ - - // We did not merge, meaning all the remaining clusters from bs - // have lower fee rate. Instead we add a directly to the final - // clusters. - default: - finalClusters = append(finalClusters, a) - } - } - - // Add any remaining clusters from bs. - for ; j < len(bs); j++ { - b := bs[j] - finalClusters = append(finalClusters, b) - } - - return finalClusters -} - // BudgetAggregator is a budget-based aggregator that creates clusters based on // deadlines and budgets of inputs. type BudgetAggregator struct { diff --git a/sweep/aggregator_test.go b/sweep/aggregator_test.go index b9b86379b..289f2780e 100644 --- a/sweep/aggregator_test.go +++ b/sweep/aggregator_test.go @@ -3,430 +3,21 @@ package sweep import ( "bytes" "errors" - "reflect" - "sort" "testing" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" - "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/stretchr/testify/require" ) -//nolint:lll var ( - testInputsA = InputsMap{ - wire.OutPoint{Hash: chainhash.Hash{}, Index: 0}: &SweeperInput{}, - wire.OutPoint{Hash: chainhash.Hash{}, Index: 1}: &SweeperInput{}, - wire.OutPoint{Hash: chainhash.Hash{}, Index: 2}: &SweeperInput{}, - } - - testInputsB = InputsMap{ - wire.OutPoint{Hash: chainhash.Hash{}, Index: 10}: &SweeperInput{}, - wire.OutPoint{Hash: chainhash.Hash{}, Index: 11}: &SweeperInput{}, - wire.OutPoint{Hash: chainhash.Hash{}, Index: 12}: &SweeperInput{}, - } - - testInputsC = InputsMap{ - wire.OutPoint{Hash: chainhash.Hash{}, Index: 0}: &SweeperInput{}, - wire.OutPoint{Hash: chainhash.Hash{}, Index: 1}: &SweeperInput{}, - wire.OutPoint{Hash: chainhash.Hash{}, Index: 2}: &SweeperInput{}, - wire.OutPoint{Hash: chainhash.Hash{}, Index: 10}: &SweeperInput{}, - wire.OutPoint{Hash: chainhash.Hash{}, Index: 11}: &SweeperInput{}, - wire.OutPoint{Hash: chainhash.Hash{}, Index: 12}: &SweeperInput{}, - } - testHeight = int32(800000) ) -// TestMergeClusters check that we properly can merge clusters together, -// according to their required locktime. -func TestMergeClusters(t *testing.T) { - t.Parallel() - - lockTime1 := uint32(100) - lockTime2 := uint32(200) - - testCases := []struct { - name string - a inputCluster - b inputCluster - res []inputCluster - }{ - { - name: "max fee rate", - a: inputCluster{ - sweepFeeRate: 5000, - inputs: testInputsA, - }, - b: inputCluster{ - sweepFeeRate: 7000, - inputs: testInputsB, - }, - res: []inputCluster{ - { - sweepFeeRate: 7000, - inputs: testInputsC, - }, - }, - }, - { - name: "same locktime", - a: inputCluster{ - lockTime: &lockTime1, - sweepFeeRate: 5000, - inputs: testInputsA, - }, - b: inputCluster{ - lockTime: &lockTime1, - sweepFeeRate: 7000, - inputs: testInputsB, - }, - res: []inputCluster{ - { - lockTime: &lockTime1, - sweepFeeRate: 7000, - inputs: testInputsC, - }, - }, - }, - { - name: "diff locktime", - a: inputCluster{ - lockTime: &lockTime1, - sweepFeeRate: 5000, - inputs: testInputsA, - }, - b: inputCluster{ - lockTime: &lockTime2, - sweepFeeRate: 7000, - inputs: testInputsB, - }, - res: []inputCluster{ - { - lockTime: &lockTime1, - sweepFeeRate: 5000, - inputs: testInputsA, - }, - { - lockTime: &lockTime2, - sweepFeeRate: 7000, - inputs: testInputsB, - }, - }, - }, - } - - for _, test := range testCases { - merged := mergeClusters(test.a, test.b) - if !reflect.DeepEqual(merged, test.res) { - t.Fatalf("[%s] unexpected result: %v", - test.name, spew.Sdump(merged)) - } - } -} - -// TestZipClusters tests that we can merge lists of inputs clusters correctly. -func TestZipClusters(t *testing.T) { - t.Parallel() - - createCluster := func(inp InputsMap, - f chainfee.SatPerKWeight) inputCluster { - - return inputCluster{ - sweepFeeRate: f, - inputs: inp, - } - } - - testCases := []struct { - name string - as []inputCluster - bs []inputCluster - res []inputCluster - }{ - { - name: "merge A into B", - as: []inputCluster{ - createCluster(testInputsA, 5000), - }, - bs: []inputCluster{ - createCluster(testInputsB, 7000), - }, - res: []inputCluster{ - createCluster(testInputsC, 7000), - }, - }, - { - name: "A can't merge with B", - as: []inputCluster{ - createCluster(testInputsA, 7000), - }, - bs: []inputCluster{ - createCluster(testInputsB, 5000), - }, - res: []inputCluster{ - createCluster(testInputsA, 7000), - createCluster(testInputsB, 5000), - }, - }, - { - name: "empty bs", - as: []inputCluster{ - createCluster(testInputsA, 7000), - }, - bs: []inputCluster{}, - res: []inputCluster{ - createCluster(testInputsA, 7000), - }, - }, - { - name: "empty as", - as: []inputCluster{}, - bs: []inputCluster{ - createCluster(testInputsB, 5000), - }, - res: []inputCluster{ - createCluster(testInputsB, 5000), - }, - }, - - { - name: "zip 3xA into 3xB", - as: []inputCluster{ - createCluster(testInputsA, 5000), - createCluster(testInputsA, 5000), - createCluster(testInputsA, 5000), - }, - bs: []inputCluster{ - createCluster(testInputsB, 7000), - createCluster(testInputsB, 7000), - createCluster(testInputsB, 7000), - }, - res: []inputCluster{ - createCluster(testInputsC, 7000), - createCluster(testInputsC, 7000), - createCluster(testInputsC, 7000), - }, - }, - { - name: "zip A into 3xB", - as: []inputCluster{ - createCluster(testInputsA, 2500), - }, - bs: []inputCluster{ - createCluster(testInputsB, 3000), - createCluster(testInputsB, 2000), - createCluster(testInputsB, 1000), - }, - res: []inputCluster{ - createCluster(testInputsC, 3000), - createCluster(testInputsB, 2000), - createCluster(testInputsB, 1000), - }, - }, - } - - for _, test := range testCases { - zipped := zipClusters(test.as, test.bs) - if !reflect.DeepEqual(zipped, test.res) { - t.Fatalf("[%s] unexpected result: %v", - test.name, spew.Sdump(zipped)) - } - } -} - -// TestClusterByLockTime tests the method clusterByLockTime works as expected. -func TestClusterByLockTime(t *testing.T) { - t.Parallel() - - // Create a mock FeePreference. - mockFeePref := &MockFeePreference{} - - // Create a test param with a dummy fee preference. This is needed so - // `feeRateForPreference` won't throw an error. - param := Params{Fee: mockFeePref} - - // We begin the test by creating three clusters of inputs, the first - // cluster has a locktime of 1, the second has a locktime of 2, and the - // final has no locktime. - lockTime1 := uint32(1) - lockTime2 := uint32(2) - - // Create cluster one, which has a locktime of 1. - input1LockTime1 := &input.MockInput{} - input2LockTime1 := &input.MockInput{} - input1LockTime1.On("RequiredLockTime").Return(lockTime1, true) - input2LockTime1.On("RequiredLockTime").Return(lockTime1, true) - - // Create cluster two, which has a locktime of 2. - input3LockTime2 := &input.MockInput{} - input4LockTime2 := &input.MockInput{} - input3LockTime2.On("RequiredLockTime").Return(lockTime2, true) - input4LockTime2.On("RequiredLockTime").Return(lockTime2, true) - - // Create cluster three, which has no locktime. - input5NoLockTime := &input.MockInput{} - input6NoLockTime := &input.MockInput{} - input5NoLockTime.On("RequiredLockTime").Return(uint32(0), false) - input6NoLockTime.On("RequiredLockTime").Return(uint32(0), false) - - // With the inner Input being mocked, we can now create the pending - // inputs. - input1 := &SweeperInput{Input: input1LockTime1, params: param} - input2 := &SweeperInput{Input: input2LockTime1, params: param} - input3 := &SweeperInput{Input: input3LockTime2, params: param} - input4 := &SweeperInput{Input: input4LockTime2, params: param} - input5 := &SweeperInput{Input: input5NoLockTime, params: param} - input6 := &SweeperInput{Input: input6NoLockTime, params: param} - - // Create the pending inputs map, which will be passed to the method - // under test. - // - // NOTE: we don't care the actual outpoint values as long as they are - // unique. - inputs := InputsMap{ - wire.OutPoint{Index: 1}: input1, - wire.OutPoint{Index: 2}: input2, - wire.OutPoint{Index: 3}: input3, - wire.OutPoint{Index: 4}: input4, - wire.OutPoint{Index: 5}: input5, - wire.OutPoint{Index: 6}: input6, - } - - // Create expected clusters so we can shorten the line length in the - // test cases below. - cluster1 := InputsMap{ - wire.OutPoint{Index: 1}: input1, - wire.OutPoint{Index: 2}: input2, - } - cluster2 := InputsMap{ - wire.OutPoint{Index: 3}: input3, - wire.OutPoint{Index: 4}: input4, - } - - // cluster3 should be the remaining inputs since they don't have - // locktime. - cluster3 := InputsMap{ - wire.OutPoint{Index: 5}: input5, - wire.OutPoint{Index: 6}: input6, - } - - const ( - // Set the min fee rate to be 1000 sat/kw. - minFeeRate = chainfee.SatPerKWeight(1000) - - // Set the max fee rate to be 10,000 sat/kw. - maxFeeRate = chainfee.SatPerKWeight(10_000) - ) - - // Create a test aggregator. - s := NewSimpleUtxoAggregator(nil, maxFeeRate, 100) - - testCases := []struct { - name string - // setupMocker takes a testing fee rate and makes a mocker over - // `Estimate` that always return the testing fee rate. - setupMocker func() - testFeeRate chainfee.SatPerKWeight - expectedClusters []inputCluster - expectedRemainingInputs InputsMap - }{ - { - // Test a successful case where the locktime clusters - // are created and the no-locktime cluster is returned - // as the remaining inputs. - name: "successfully create clusters", - setupMocker: func() { - // Expect the four inputs with locktime to call - // this method. - mockFeePref.On("Estimate", nil, maxFeeRate). - Return(minFeeRate+1, nil).Times(4) - }, - // Use a fee rate above the min value so we don't hit - // an error when performing fee estimation. - // - // TODO(yy): we should customize the returned fee rate - // for each input to further test the averaging logic. - // Or we can split the method into two, one for - // grouping the clusters and the other for averaging - // the fee rates so it's easier to be tested. - testFeeRate: minFeeRate + 1, - expectedClusters: []inputCluster{ - { - lockTime: &lockTime1, - sweepFeeRate: minFeeRate + 1, - inputs: cluster1, - }, - { - lockTime: &lockTime2, - sweepFeeRate: minFeeRate + 1, - inputs: cluster2, - }, - }, - expectedRemainingInputs: cluster3, - }, - { - // Test that when the input is skipped when the fee - // estimation returns an error. - name: "error from fee estimation", - setupMocker: func() { - mockFeePref.On("Estimate", nil, maxFeeRate). - Return(chainfee.SatPerKWeight(0), - errors.New("dummy")).Times(4) - }, - - // Use a fee rate below the min value so we hit an - // error when performing fee estimation. - testFeeRate: minFeeRate - 1, - expectedClusters: []inputCluster{}, - // Remaining inputs should stay untouched. - expectedRemainingInputs: cluster3, - }, - } - - //nolint:paralleltest - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - // Apply the test fee rate so `feeRateForPreference` is - // mocked to return the specified value. - tc.setupMocker() - - // Assert the mocked methods are called as expeceted. - defer mockFeePref.AssertExpectations(t) - - // Call the method under test. - clusters, remainingInputs := s.clusterByLockTime(inputs) - - // Sort by locktime as the order is not guaranteed. - sort.Slice(clusters, func(i, j int) bool { - return *clusters[i].lockTime < - *clusters[j].lockTime - }) - - // Validate the values are returned as expected. - require.Equal(t, tc.expectedClusters, clusters) - require.Equal(t, tc.expectedRemainingInputs, - remainingInputs, - ) - - // Assert the mocked methods are called as expeceted. - input1LockTime1.AssertExpectations(t) - input2LockTime1.AssertExpectations(t) - input3LockTime2.AssertExpectations(t) - input4LockTime2.AssertExpectations(t) - input5NoLockTime.AssertExpectations(t) - input6NoLockTime.AssertExpectations(t) - }) - } -} - // TestBudgetAggregatorFilterInputs checks that inputs with low budget are // filtered out. func TestBudgetAggregatorFilterInputs(t *testing.T) { diff --git a/sweep/bucket_list.go b/sweep/bucket_list.go deleted file mode 100644 index e5a2cfea6..000000000 --- a/sweep/bucket_list.go +++ /dev/null @@ -1,56 +0,0 @@ -package sweep - -// bucket contains a set of inputs that are not mutually exclusive. -type bucket InputsMap - -// tryAdd tries to add a new input to this bucket. -func (b bucket) tryAdd(input *SweeperInput) bool { - exclusiveGroup := input.params.ExclusiveGroup - if exclusiveGroup != nil { - for _, input := range b { - existingGroup := input.params.ExclusiveGroup - - // Don't add an exclusive group input if other inputs - // are non-exclusive. The exclusive group input may be - // invalid (for example in the case of commitment - // anchors) and could thereby block sweeping of the - // other inputs. - if existingGroup == nil { - return false - } - - // Don't combine inputs from the same exclusive group. - // Because only one input is valid, this may result in - // txes that are always invalid. - if *existingGroup == *exclusiveGroup { - return false - } - } - } - - b[input.OutPoint()] = input - - return true -} - -// bucketList is a list of buckets that contain non-mutually exclusive inputs. -type bucketList struct { - buckets []bucket -} - -// add adds a new input. If the input is not accepted by any of the existing -// buckets, a new bucket will be created. -func (b *bucketList) add(input *SweeperInput) { - for _, existingBucket := range b.buckets { - if existingBucket.tryAdd(input) { - return - } - } - - // Create a new bucket and add the input. It is not necessary to check - // the return value of tryAdd because it will always succeed on an empty - // bucket. - newBucket := make(bucket) - newBucket.tryAdd(input) - b.buckets = append(b.buckets, newBucket) -} diff --git a/sweep/fee_bumper_test.go b/sweep/fee_bumper_test.go index e2b2cfe14..63a828654 100644 --- a/sweep/fee_bumper_test.go +++ b/sweep/fee_bumper_test.go @@ -2,14 +2,17 @@ package sweep import ( "fmt" + "sync/atomic" "testing" "time" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/stretchr/testify/mock" @@ -25,8 +28,37 @@ var ( 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, } + + testInputCount atomic.Uint64 ) +func createTestInput(value int64, + witnessType input.WitnessType) input.BaseInput { + + hash := chainhash.Hash{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + byte(testInputCount.Add(1))} + + input := input.MakeBaseInput( + &wire.OutPoint{ + Hash: hash, + }, + witnessType, + &input.SignDescriptor{ + Output: &wire.TxOut{ + Value: value, + }, + KeyDesc: keychain.KeyDescriptor{ + PubKey: testPubKey, + }, + }, + 0, + nil, + ) + + return input +} + // TestBumpResultValidate tests the validate method of the BumpResult struct. func TestBumpResultValidate(t *testing.T) { t.Parallel() diff --git a/sweep/mock_test.go b/sweep/mock_test.go index 356d2e3a8..605b8d14e 100644 --- a/sweep/mock_test.go +++ b/sweep/mock_test.go @@ -1,10 +1,6 @@ package sweep import ( - "sync" - "testing" - "time" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -15,251 +11,6 @@ import ( "github.com/stretchr/testify/mock" ) -// mockBackend simulates a chain backend for realistic behaviour in unit tests -// around double spends. -type mockBackend struct { - t *testing.T - - lock sync.Mutex - - notifier *MockNotifier - - confirmedSpendInputs map[wire.OutPoint]struct{} - - unconfirmedTxes map[chainhash.Hash]*wire.MsgTx - unconfirmedSpendInputs map[wire.OutPoint]struct{} - - publishChan chan wire.MsgTx - - walletUtxos []*lnwallet.Utxo - utxoCnt int -} - -func newMockBackend(t *testing.T, notifier *MockNotifier) *mockBackend { - return &mockBackend{ - t: t, - notifier: notifier, - unconfirmedTxes: make(map[chainhash.Hash]*wire.MsgTx), - confirmedSpendInputs: make(map[wire.OutPoint]struct{}), - unconfirmedSpendInputs: make(map[wire.OutPoint]struct{}), - publishChan: make(chan wire.MsgTx, 2), - } -} - -func (b *mockBackend) BackEnd() string { - return "mockbackend" -} - -func (b *mockBackend) CheckMempoolAcceptance(tx *wire.MsgTx) error { - return nil -} - -func (b *mockBackend) publishTransaction(tx *wire.MsgTx) error { - b.lock.Lock() - defer b.lock.Unlock() - - txHash := tx.TxHash() - if _, ok := b.unconfirmedTxes[txHash]; ok { - // Tx already exists - testLog.Tracef("mockBackend duplicate tx %v", tx.TxHash()) - return lnwallet.ErrDoubleSpend - } - - for _, in := range tx.TxIn { - if _, ok := b.unconfirmedSpendInputs[in.PreviousOutPoint]; ok { - // Double spend - testLog.Tracef("mockBackend double spend tx %v", - tx.TxHash()) - return lnwallet.ErrDoubleSpend - } - - if _, ok := b.confirmedSpendInputs[in.PreviousOutPoint]; ok { - // Already included in block - testLog.Tracef("mockBackend already in block tx %v", - tx.TxHash()) - return lnwallet.ErrDoubleSpend - } - } - - b.unconfirmedTxes[txHash] = tx - for _, in := range tx.TxIn { - b.unconfirmedSpendInputs[in.PreviousOutPoint] = struct{}{} - } - - testLog.Tracef("mockBackend publish tx %v", tx.TxHash()) - - return nil -} - -func (b *mockBackend) PublishTransaction(tx *wire.MsgTx, _ string) error { - log.Tracef("Publishing tx %v", tx.TxHash()) - err := b.publishTransaction(tx) - select { - case b.publishChan <- *tx: - case <-time.After(defaultTestTimeout): - b.t.Fatalf("unexpected tx published") - } - - return err -} - -func (b *mockBackend) ListUnspentWitnessFromDefaultAccount(minConfs, - maxConfs int32) ([]*lnwallet.Utxo, error) { - - b.lock.Lock() - defer b.lock.Unlock() - - // Each time we list output, we increment the utxo counter, to - // ensure we don't return the same outpoint every time. - b.utxoCnt++ - - for i := range b.walletUtxos { - b.walletUtxos[i].OutPoint.Hash[0] = byte(b.utxoCnt) - } - - return b.walletUtxos, nil -} - -func (b *mockBackend) WithCoinSelectLock(f func() error) error { - return f() -} - -func (b *mockBackend) deleteUnconfirmed(txHash chainhash.Hash) { - b.lock.Lock() - defer b.lock.Unlock() - - tx, ok := b.unconfirmedTxes[txHash] - if !ok { - // Tx already exists - testLog.Errorf("mockBackend delete tx not existing %v", txHash) - return - } - - testLog.Tracef("mockBackend delete tx %v", tx.TxHash()) - delete(b.unconfirmedTxes, txHash) - for _, in := range tx.TxIn { - delete(b.unconfirmedSpendInputs, in.PreviousOutPoint) - } -} - -func (b *mockBackend) mine() { - b.lock.Lock() - defer b.lock.Unlock() - - notifications := make(map[wire.OutPoint]*wire.MsgTx) - for _, tx := range b.unconfirmedTxes { - testLog.Tracef("mockBackend mining tx %v", tx.TxHash()) - for _, in := range tx.TxIn { - b.confirmedSpendInputs[in.PreviousOutPoint] = struct{}{} - notifications[in.PreviousOutPoint] = tx - } - } - b.unconfirmedSpendInputs = make(map[wire.OutPoint]struct{}) - b.unconfirmedTxes = make(map[chainhash.Hash]*wire.MsgTx) - - for outpoint, tx := range notifications { - testLog.Tracef("mockBackend delivering spend ntfn for %v", - outpoint) - b.notifier.SpendOutpoint(outpoint, *tx) - } -} - -func (b *mockBackend) isDone() bool { - return len(b.unconfirmedTxes) == 0 -} - -func (b *mockBackend) RemoveDescendants(*wire.MsgTx) error { - return nil -} - -func (b *mockBackend) FetchTx(chainhash.Hash) (*wire.MsgTx, error) { - return nil, nil -} - -func (b *mockBackend) CancelRebroadcast(tx chainhash.Hash) { -} - -// GetTransactionDetails returns a detailed description of a tx given its -// transaction hash. -func (b *mockBackend) GetTransactionDetails(txHash *chainhash.Hash) ( - *lnwallet.TransactionDetail, error) { - - return nil, nil -} - -// mockFeeEstimator implements a mock fee estimator. It closely resembles -// lnwallet.StaticFeeEstimator with the addition that fees can be changed for -// testing purposes in a thread safe manner. -// -// TODO(yy): replace it with chainfee.MockEstimator once it's merged. -type mockFeeEstimator struct { - feePerKW chainfee.SatPerKWeight - - relayFee chainfee.SatPerKWeight - - blocksToFee map[uint32]chainfee.SatPerKWeight - - // A closure that when set is used instead of the - // mockFeeEstimator.EstimateFeePerKW method. - estimateFeePerKW func(numBlocks uint32) (chainfee.SatPerKWeight, error) - - lock sync.Mutex -} - -func newMockFeeEstimator(feePerKW, - relayFee chainfee.SatPerKWeight) *mockFeeEstimator { - - return &mockFeeEstimator{ - feePerKW: feePerKW, - relayFee: relayFee, - blocksToFee: make(map[uint32]chainfee.SatPerKWeight), - } -} - -func (e *mockFeeEstimator) updateFees(feePerKW, - relayFee chainfee.SatPerKWeight) { - - e.lock.Lock() - defer e.lock.Unlock() - - e.feePerKW = feePerKW - e.relayFee = relayFee -} - -func (e *mockFeeEstimator) EstimateFeePerKW(numBlocks uint32) ( - chainfee.SatPerKWeight, error) { - - e.lock.Lock() - defer e.lock.Unlock() - - if e.estimateFeePerKW != nil { - return e.estimateFeePerKW(numBlocks) - } - - if fee, ok := e.blocksToFee[numBlocks]; ok { - return fee, nil - } - - return e.feePerKW, nil -} - -func (e *mockFeeEstimator) RelayFeePerKW() chainfee.SatPerKWeight { - e.lock.Lock() - defer e.lock.Unlock() - - return e.relayFee -} - -func (e *mockFeeEstimator) Start() error { - return nil -} - -func (e *mockFeeEstimator) Stop() error { - return nil -} - -var _ chainfee.Estimator = (*mockFeeEstimator)(nil) - // MockSweeperStore is a mock implementation of sweeper store. This type is // exported, because it is currently used in nursery tests too. type MockSweeperStore struct { diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 4abf59d1a..2754485ea 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -43,11 +43,6 @@ var ( // Params contains the parameters that control the sweeping process. type Params struct { - // Fee is the fee preference of the client who requested the input to be - // swept. If a confirmation target is specified, then we'll map it into - // a fee rate whenever we attempt to cluster inputs for a sweep. - Fee FeePreference - // ExclusiveGroup is an identifier that, if set, prevents other inputs // with the same identifier from being batched together. ExclusiveGroup *uint64 @@ -211,13 +206,6 @@ func (p *SweeperInput) String() string { return fmt.Sprintf("%v (%v)", p.Input.OutPoint(), p.Input.WitnessType()) } -// parameters returns the sweep parameters for this input. -// -// NOTE: Part of the txInput interface. -func (p *SweeperInput) parameters() Params { - return p.params -} - // terminated returns a boolean indicating whether the input has reached a // final state. func (p *SweeperInput) terminated() bool { @@ -512,18 +500,6 @@ func (s *UtxoSweeper) SweepInput(inp input.Input, return nil, errors.New("nil input received") } - // Ensure the client provided a sane fee preference. - // - // TODO(yy): remove this check? - if params.Fee != nil { - _, err := params.Fee.Estimate( - s.cfg.FeeEstimator, s.cfg.MaxFeeRate.FeePerKWeight(), - ) - if err != nil { - return nil, err - } - } - absoluteTimeLock, _ := inp.RequiredLockTime() log.Infof("Sweep request received: out_point=%v, witness_type=%v, "+ "relative_time_lock=%v, absolute_time_lock=%v, amount=%v, "+ @@ -1145,7 +1121,6 @@ func (s *UtxoSweeper) handleUpdateReq(req *updateReq) ( // Create the updated parameters struct. Leave the exclusive group // unchanged. newParams := Params{ - Fee: req.params.Fee, StartingFeeRate: req.params.StartingFeeRate, Immediate: req.params.Immediate, Budget: req.params.Budget, diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index bfa3ff778..e25864bcb 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -2,61 +2,23 @@ package sweep import ( "errors" - "fmt" - "os" - "runtime/pprof" - "sync/atomic" "testing" "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" - "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/chainntnfs" - "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" - "github.com/lightningnetwork/lnd/keychain" - lnmock "github.com/lightningnetwork/lnd/lntest/mock" - "github.com/lightningnetwork/lnd/lntest/wait" - "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) var ( - testLog = build.NewSubLogger("SWPR_TEST", nil) - - testMaxSweepAttempts = 3 - - testMaxInputsPerTx = uint32(3) - - defaultFeePref = Params{Fee: FeeEstimateInfo{ConfTarget: 1}} - errDummy = errors.New("dummy error") -) - -type sweeperTestContext struct { - t *testing.T - - sweeper *UtxoSweeper - notifier *MockNotifier - estimator *mockFeeEstimator - backend *mockBackend - store SweeperStore - publisher *MockBumper - - publishChan chan wire.MsgTx - currentHeight int32 -} - -var ( - spendableInputs []*input.BaseInput - testInputCount atomic.Uint64 testPubKey, _ = btcec.ParsePubKey([]byte{ 0x04, 0x11, 0xdb, 0x93, 0xe1, 0xdc, 0xdb, 0x8a, @@ -70,2051 +32,6 @@ var ( }) ) -func createTestInput(value int64, witnessType input.WitnessType) input.BaseInput { - hash := chainhash.Hash{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - byte(testInputCount.Add(1))} - - input := input.MakeBaseInput( - &wire.OutPoint{ - Hash: hash, - }, - witnessType, - &input.SignDescriptor{ - Output: &wire.TxOut{ - Value: value, - }, - KeyDesc: keychain.KeyDescriptor{ - PubKey: testPubKey, - }, - }, - 0, - nil, - ) - - return input -} - -func init() { - // Create a set of test spendable inputs. - for i := 0; i < 20; i++ { - input := createTestInput(int64(10000+i*500), - input.CommitmentTimeLock) - - spendableInputs = append(spendableInputs, &input) - } -} - -func createSweeperTestContext(t *testing.T) *sweeperTestContext { - notifier := NewMockNotifier(t) - - // Create new store. - cdb, err := channeldb.MakeTestDB(t) - require.NoError(t, err) - - var chain chainhash.Hash - store, err := NewSweeperStore(cdb, &chain) - require.NoError(t, err) - - backend := newMockBackend(t, notifier) - backend.walletUtxos = []*lnwallet.Utxo{ - { - Value: btcutil.Amount(1_000_000), - AddressType: lnwallet.WitnessPubKey, - }, - } - - estimator := newMockFeeEstimator(10000, chainfee.FeePerKwFloor) - - aggregator := NewSimpleUtxoAggregator( - estimator, DefaultMaxFeeRate.FeePerKWeight(), - testMaxInputsPerTx, - ) - - // Create a mock fee bumper. - mockBumper := &MockBumper{} - t.Cleanup(func() { - mockBumper.AssertExpectations(t) - }) - - ctx := &sweeperTestContext{ - notifier: notifier, - publishChan: backend.publishChan, - t: t, - estimator: estimator, - backend: backend, - store: store, - currentHeight: mockChainHeight, - publisher: mockBumper, - } - - ctx.sweeper = New(&UtxoSweeperConfig{ - Notifier: notifier, - Wallet: backend, - Store: store, - Signer: &lnmock.DummySigner{}, - GenSweepScript: func() ([]byte, error) { - script := make([]byte, input.P2WPKHSize) - script[0] = 0 - script[1] = 20 - return script, nil - }, - FeeEstimator: estimator, - MaxInputsPerTx: testMaxInputsPerTx, - MaxFeeRate: DefaultMaxFeeRate, - Aggregator: aggregator, - Publisher: mockBumper, - }) - - ctx.sweeper.Start() - - return ctx -} - -func (ctx *sweeperTestContext) restartSweeper() { - ctx.t.Helper() - - ctx.sweeper.Stop() - ctx.sweeper = New(ctx.sweeper.cfg) - ctx.sweeper.Start() -} - -func (ctx *sweeperTestContext) finish(expectedGoroutineCount int) { - // We assume that when finish is called, sweeper has finished all its - // goroutines. This implies that the waitgroup is empty. - signalChan := make(chan struct{}) - go func() { - ctx.sweeper.wg.Wait() - close(signalChan) - }() - - // Simulate exits of the expected number of running goroutines. - for i := 0; i < expectedGoroutineCount; i++ { - ctx.sweeper.wg.Done() - } - - // We now expect the Wait to succeed. - select { - case <-signalChan: - case <-time.After(time.Second): - pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) - - ctx.t.Fatalf("lingering goroutines detected after test " + - "is finished") - } - - // Restore waitgroup state to what it was before. - ctx.sweeper.wg.Add(expectedGoroutineCount) - - // Stop sweeper. - ctx.sweeper.Stop() - - // We should have consumed and asserted all published transactions in - // our unit tests. - ctx.assertNoTx() - if !ctx.backend.isDone() { - ctx.t.Fatal("unconfirmed txes remaining") - } -} - -func (ctx *sweeperTestContext) assertNoTx() { - ctx.t.Helper() - select { - case <-ctx.publishChan: - ctx.t.Fatalf("unexpected transactions published") - default: - } -} - -func (ctx *sweeperTestContext) receiveTx() wire.MsgTx { - ctx.t.Helper() - - // Every time we want to receive a tx, we send a new block epoch to the - // sweeper to trigger a sweeping action. - ctx.notifier.NotifyEpochNonBlocking(ctx.currentHeight + 1) - - var tx wire.MsgTx - select { - case tx = <-ctx.publishChan: - return tx - case <-time.After(5 * time.Second): - pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) - - ctx.t.Fatalf("tx not published") - } - return tx -} - -func (ctx *sweeperTestContext) expectResult(c chan Result, expected error) { - ctx.t.Helper() - select { - case result := <-c: - if result.Err != expected { - ctx.t.Fatalf("expected %v result, but got %v", - expected, result.Err, - ) - } - case <-time.After(defaultTestTimeout): - ctx.t.Fatalf("no result received") - } -} - -func (ctx *sweeperTestContext) assertPendingInputs(inputs ...input.Input) { - ctx.t.Helper() - - inputSet := make(map[wire.OutPoint]struct{}, len(inputs)) - for _, input := range inputs { - inputSet[input.OutPoint()] = struct{}{} - } - - inputsMap, err := ctx.sweeper.PendingInputs() - if err != nil { - ctx.t.Fatal(err) - } - if len(inputsMap) != len(inputSet) { - ctx.t.Fatalf("expected %d pending inputs, got %d", - len(inputSet), len(inputsMap)) - } - for input := range inputsMap { - if _, ok := inputSet[input]; !ok { - ctx.t.Fatalf("found unexpected input %v", input) - } - } -} - -// assertTxSweepsInputs ensures that the transaction returned within the value -// received from resultChan spends the given inputs. -func assertTxSweepsInputs(t *testing.T, sweepTx *wire.MsgTx, - inputs ...input.Input) { - - t.Helper() - - if len(sweepTx.TxIn) != len(inputs) { - t.Fatalf("expected sweep tx to contain %d inputs, got %d", - len(inputs), len(sweepTx.TxIn)) - } - m := make(map[wire.OutPoint]struct{}, len(inputs)) - for _, input := range inputs { - m[input.OutPoint()] = struct{}{} - } - for _, txIn := range sweepTx.TxIn { - if _, ok := m[txIn.PreviousOutPoint]; !ok { - t.Fatalf("expected tx %v to spend input %v", - txIn.PreviousOutPoint, sweepTx.TxHash()) - } - } -} - -// assertTxFeeRate asserts that the transaction was created with the given -// inputs and fee rate. -// -// NOTE: This assumes that transactions only have one output, as this is the -// only type of transaction the UtxoSweeper can create at the moment. -func assertTxFeeRate(t *testing.T, tx *wire.MsgTx, - expectedFeeRate chainfee.SatPerKWeight, changePk []byte, - inputs ...input.Input) { - - t.Helper() - - if len(tx.TxIn) != len(inputs) { - t.Fatalf("expected %d inputs, got %d", len(tx.TxIn), len(inputs)) - } - - m := make(map[wire.OutPoint]input.Input, len(inputs)) - for _, input := range inputs { - m[input.OutPoint()] = input - } - - var inputAmt int64 - for _, txIn := range tx.TxIn { - input, ok := m[txIn.PreviousOutPoint] - if !ok { - t.Fatalf("expected input %v to be provided", - txIn.PreviousOutPoint) - } - inputAmt += input.SignDesc().Output.Value - } - outputAmt := tx.TxOut[0].Value - - fee := btcutil.Amount(inputAmt - outputAmt) - _, estimator, err := getWeightEstimate(inputs, nil, 0, 0, changePk) - require.NoError(t, err) - - txWeight := estimator.weight() - - expectedFee := expectedFeeRate.FeeForWeight(int64(txWeight)) - if fee != expectedFee { - t.Fatalf("expected fee rate %v results in %v fee, got %v fee", - expectedFeeRate, expectedFee, fee) - } -} - -// assertNumSweeps asserts that the expected number of sweeps has been found in -// the sweeper's store. -func assertNumSweeps(t *testing.T, sweeper *UtxoSweeper, num int) { - err := wait.NoError(func() error { - sweeps, err := sweeper.ListSweeps() - if err != nil { - return err - } - - if len(sweeps) != num { - return fmt.Errorf("want %d sweeps, got %d", - num, len(sweeps)) - } - - return nil - }, 5*time.Second) - require.NoError(t, err, "timeout checking num of sweeps") -} - -// TestSuccess tests the sweeper happy flow. -func TestSuccess(t *testing.T) { - ctx := createSweeperTestContext(t) - - inp := spendableInputs[0] - - // Sweeping an input without a fee preference should result in an error. - _, err := ctx.sweeper.SweepInput(inp, Params{ - Fee: &FeeEstimateInfo{}, - }) - require.ErrorIs(t, err, ErrNoFeePreference) - - // Mock the Broadcast method to succeed. - bumpResultChan := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{{ - PreviousOutPoint: inp.OutPoint(), - }}, - } - - // Send the first event. - bumpResultChan <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }) - - resultChan, err := ctx.sweeper.SweepInput(inp, defaultFeePref) - require.NoError(t, err) - - sweepTx := ctx.receiveTx() - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 1) - - // Mock a confirmed event. - bumpResultChan <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx, - FeeRate: 10, - Fee: 100, - } - - // Mine a block to confirm the sweep tx. - ctx.backend.mine() - - select { - case result := <-resultChan: - if result.Err != nil { - t.Fatalf("expected successful spend, but received "+ - "error %v instead", result.Err) - } - if result.Tx.TxHash() != sweepTx.TxHash() { - t.Fatalf("expected sweep tx ") - } - case <-time.After(5 * time.Second): - t.Fatalf("no result received") - } - - ctx.finish(1) -} - -// TestDust asserts that inputs that are not big enough to raise above the dust -// limit, are held back until the total set does surpass the limit. -func TestDust(t *testing.T) { - ctx := createSweeperTestContext(t) - - // Sweeping a single output produces a tx of 486 weight units. With the - // test fee rate, the sweep tx will pay 4860 sat in fees. - // - // Create an input so that the output after paying fees is still - // positive (400 sat), but less than the dust limit (537 sat) for the - // sweep tx output script (P2WPKH). - dustInput := createTestInput(5260, input.CommitmentTimeLock) - - _, err := ctx.sweeper.SweepInput(&dustInput, defaultFeePref) - require.NoError(t, err) - - // No sweep transaction is expected now. The sweeper should recognize - // that the sweep output will not be relayed and not generate the tx. It - // isn't possible to attach a wallet utxo either, because the added - // weight would create a negatively yielding transaction at this fee - // rate. - - // Sweep another input that brings the tx output above the dust limit. - largeInput := createTestInput(100000, input.CommitmentTimeLock) - - // Mock the Broadcast method to succeed. - bumpResultChan := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: largeInput.OutPoint()}, - {PreviousOutPoint: dustInput.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }) - - _, err = ctx.sweeper.SweepInput(&largeInput, defaultFeePref) - require.NoError(t, err) - - // The second input brings the sweep output above the dust limit. We - // expect a sweep tx now. - sweepTx := ctx.receiveTx() - require.Len(t, sweepTx.TxIn, 2, "unexpected num of tx inputs") - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 1) - - // Mock a confirmed event. - bumpResultChan <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx, - FeeRate: 10, - Fee: 100, - } - - ctx.backend.mine() - - ctx.finish(1) -} - -// TestWalletUtxo asserts that inputs that are not big enough to raise above the -// dust limit are accompanied by a wallet utxo to make them sweepable. -func TestWalletUtxo(t *testing.T) { - ctx := createSweeperTestContext(t) - - // Sweeping a single output produces a tx of 439 weight units. At the - // fee floor, the sweep tx will pay 439*253/1000 = 111 sat in fees. - // - // Create an input so that the output after paying fees is still - // positive (183 sat), but less than the dust limit (537 sat) for the - // sweep tx output script (P2WPKH). - // - // What we now expect is that the sweeper will attach a utxo from the - // wallet. This increases the tx weight to 712 units with a fee of 180 - // sats. The tx yield becomes then 294-180 = 114 sats. - dustInput := createTestInput(294, input.WitnessKeyHash) - - // Mock the Broadcast method to succeed. - bumpResultChan := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: dustInput.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }) - - _, err := ctx.sweeper.SweepInput( - &dustInput, - Params{Fee: FeeEstimateInfo{FeeRate: chainfee.FeePerKwFloor}}, - ) - require.NoError(t, err) - - sweepTx := ctx.receiveTx() - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 1) - - ctx.backend.mine() - - // Mock a confirmed event. - bumpResultChan <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx, - FeeRate: 10, - Fee: 100, - } - - ctx.finish(1) -} - -// TestNegativeInput asserts that no inputs with a negative yield are swept. -// Negative yield means that the value minus the added fee is negative. -func TestNegativeInput(t *testing.T) { - ctx := createSweeperTestContext(t) - - // Sweep an input large enough to cover fees, so in any case the tx - // output will be above the dust limit. - largeInput := createTestInput(100000, input.CommitmentNoDelay) - largeInputResult, err := ctx.sweeper.SweepInput( - &largeInput, defaultFeePref, - ) - require.NoError(t, err) - - // Sweep an additional input with a negative net yield. The weight of - // the HtlcAcceptedRemoteSuccess input type adds more in fees than its - // value at the current fee level. - negInput := createTestInput(2900, input.HtlcOfferedRemoteTimeout) - negInputResult, err := ctx.sweeper.SweepInput(&negInput, defaultFeePref) - require.NoError(t, err) - - // Sweep a third input that has a smaller output than the previous one, - // but yields positively because of its lower weight. - positiveInput := createTestInput(2800, input.CommitmentNoDelay) - - // Mock the Broadcast method to succeed. - bumpResultChan := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: largeInput.OutPoint()}, - {PreviousOutPoint: positiveInput.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - positiveInputResult, err := ctx.sweeper.SweepInput( - &positiveInput, defaultFeePref, - ) - require.NoError(t, err) - - // We expect that a sweep tx is published now, but it should only - // contain the large input. The negative input should stay out of sweeps - // until fees come down to get a positive net yield. - sweepTx1 := ctx.receiveTx() - assertTxSweepsInputs(t, &sweepTx1, &largeInput, &positiveInput) - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 1) - - ctx.backend.mine() - - // Mock a confirmed event. - bumpResultChan <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx1, - FeeRate: 10, - Fee: 100, - } - - ctx.expectResult(largeInputResult, nil) - ctx.expectResult(positiveInputResult, nil) - - // Lower fee rate so that the negative input is no longer negative. - ctx.estimator.updateFees(1000, 1000) - - // Create another large input. - secondLargeInput := createTestInput(100000, input.CommitmentNoDelay) - - // Mock the Broadcast method to succeed. - bumpResultChan = make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: negInput.OutPoint()}, - {PreviousOutPoint: secondLargeInput.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - secondLargeInputResult, err := ctx.sweeper.SweepInput( - &secondLargeInput, defaultFeePref, - ) - require.NoError(t, err) - - sweepTx2 := ctx.receiveTx() - assertTxSweepsInputs(t, &sweepTx2, &secondLargeInput, &negInput) - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 2) - - ctx.backend.mine() - - // Mock a confirmed event. - bumpResultChan <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx2, - FeeRate: 10, - Fee: 100, - } - - ctx.expectResult(secondLargeInputResult, nil) - ctx.expectResult(negInputResult, nil) - - ctx.finish(1) -} - -// TestChunks asserts that large sets of inputs are split into multiple txes. -func TestChunks(t *testing.T) { - ctx := createSweeperTestContext(t) - - // Mock the Broadcast method to succeed on the first chunk. - bumpResultChan1 := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan1, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - //nolint:lll - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: spendableInputs[0].OutPoint()}, - {PreviousOutPoint: spendableInputs[1].OutPoint()}, - {PreviousOutPoint: spendableInputs[2].OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan1 <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - // Mock the Broadcast method to succeed on the second chunk. - bumpResultChan2 := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan2, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - //nolint:lll - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: spendableInputs[3].OutPoint()}, - {PreviousOutPoint: spendableInputs[4].OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan2 <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - // Sweep five inputs. - for _, input := range spendableInputs[:5] { - _, err := ctx.sweeper.SweepInput(input, defaultFeePref) - require.NoError(t, err) - } - - // We expect two txes to be published because of the max input count of - // three. - sweepTx1 := ctx.receiveTx() - require.Len(t, sweepTx1.TxIn, 3) - - sweepTx2 := ctx.receiveTx() - require.Len(t, sweepTx2.TxIn, 2) - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 2) - - ctx.backend.mine() - - // Mock a confirmed event. - bumpResultChan1 <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx1, - FeeRate: 10, - Fee: 100, - } - bumpResultChan2 <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx2, - FeeRate: 10, - Fee: 100, - } - - ctx.finish(1) -} - -// TestRemoteSpend asserts that remote spends are properly detected and handled -// both before the sweep is published as well as after. -func TestRemoteSpend(t *testing.T) { - t.Run("pre-sweep", func(t *testing.T) { - testRemoteSpend(t, false) - }) - t.Run("post-sweep", func(t *testing.T) { - testRemoteSpend(t, true) - }) -} - -func testRemoteSpend(t *testing.T, postSweep bool) { - ctx := createSweeperTestContext(t) - - // Create a fake sweep tx that spends the second input as the first - // will be spent by the remote. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: spendableInputs[1].OutPoint()}, - }, - } - - // Mock the Broadcast method to succeed. - bumpResultChan := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan, nil).Run(func(args mock.Arguments) { - // Send the first event. - bumpResultChan <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - resultChan1, err := ctx.sweeper.SweepInput( - spendableInputs[0], defaultFeePref, - ) - require.NoError(t, err) - - resultChan2, err := ctx.sweeper.SweepInput( - spendableInputs[1], defaultFeePref, - ) - require.NoError(t, err) - - // Spend the input with an unknown tx. - remoteTx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: spendableInputs[0].OutPoint()}, - }, - } - err = ctx.backend.publishTransaction(remoteTx) - require.NoError(t, err) - - if postSweep { - // Tx publication by sweeper returns ErrDoubleSpend. Sweeper - // will retry the inputs without reporting a result. It could be - // spent by the remote party. - ctx.receiveTx() - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 1) - } - - ctx.backend.mine() - - select { - case result := <-resultChan1: - if result.Err != ErrRemoteSpend { - t.Fatalf("expected remote spend") - } - if result.Tx.TxHash() != remoteTx.TxHash() { - t.Fatalf("expected remote spend tx") - } - case <-time.After(5 * time.Second): - t.Fatalf("no result received") - } - - if !postSweep { - // Assert that the sweeper sweeps the remaining input. - sweepTx := ctx.receiveTx() - require.Len(t, sweepTx.TxIn, 1) - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 1) - - ctx.backend.mine() - - // Mock a confirmed event. - bumpResultChan <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx, - FeeRate: 10, - Fee: 100, - } - - ctx.expectResult(resultChan2, nil) - - ctx.finish(1) - } else { - // Expected sweeper to be still listening for spend of the - // error input. - ctx.finish(2) - - select { - case r := <-resultChan2: - require.NoError(t, r.Err) - require.Equal(t, r.Tx.TxHash(), tx.TxHash()) - - default: - } - } -} - -// TestIdempotency asserts that offering the same input multiple times is -// handled correctly. -func TestIdempotency(t *testing.T) { - ctx := createSweeperTestContext(t) - - input := spendableInputs[0] - - // Mock the Broadcast method to succeed. - bumpResultChan := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: input.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - resultChan1, err := ctx.sweeper.SweepInput(input, defaultFeePref) - require.NoError(t, err) - - resultChan2, err := ctx.sweeper.SweepInput(input, defaultFeePref) - require.NoError(t, err) - - sweepTx := ctx.receiveTx() - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 1) - - resultChan3, err := ctx.sweeper.SweepInput(input, defaultFeePref) - require.NoError(t, err) - - // Spend the input of the sweep tx. - ctx.backend.mine() - - // Mock a confirmed event. - bumpResultChan <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx, - FeeRate: 10, - Fee: 100, - } - - ctx.expectResult(resultChan1, nil) - ctx.expectResult(resultChan2, nil) - ctx.expectResult(resultChan3, nil) - - // Offer the same input again. The sweeper will register a spend ntfn - // for this input. Because the input has already been spent, it will - // immediately receive the spend notification with a spending tx hash. - // Because the sweeper kept track of all of its sweep txes, it will - // recognize the spend as its own. - resultChan4, err := ctx.sweeper.SweepInput(input, defaultFeePref) - require.NoError(t, err) - ctx.expectResult(resultChan4, nil) - - // Timer is still running, but spend notification was delivered before - // it expired. - ctx.finish(1) -} - -// TestNoInputs asserts that nothing happens if nothing happens. -func TestNoInputs(t *testing.T) { - ctx := createSweeperTestContext(t) - - // No tx should appear. This is asserted in finish(). - ctx.finish(1) -} - -// TestRestart asserts that the sweeper picks up sweeping properly after -// a restart. -func TestRestart(t *testing.T) { - ctx := createSweeperTestContext(t) - - // Sweep input and expect sweep tx. - input1 := spendableInputs[0] - - // Mock the Broadcast method to succeed. - bumpResultChan1 := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan1, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: input1.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan1 <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - _, err := ctx.sweeper.SweepInput(input1, defaultFeePref) - require.NoError(t, err) - - sweepTx1 := ctx.receiveTx() - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 1) - - // Restart sweeper. - ctx.restartSweeper() - - // Simulate other subsystem (e.g. contract resolver) re-offering inputs. - spendChan1, err := ctx.sweeper.SweepInput(input1, defaultFeePref) - require.NoError(t, err) - - input2 := spendableInputs[1] - - // Mock the Broadcast method to succeed. - bumpResultChan2 := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan2, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: input2.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan2 <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - spendChan2, err := ctx.sweeper.SweepInput(input2, defaultFeePref) - require.NoError(t, err) - - // Spend inputs of sweep txes and verify that spend channels signal - // spends. - ctx.backend.mine() - - // Sweeper should recognize that its sweep tx of the previous run is - // spending the input. - select { - case result := <-spendChan1: - if result.Err != nil { - t.Fatalf("expected successful sweep") - } - case <-time.After(defaultTestTimeout): - t.Fatalf("no result received") - } - - // Timer tick should trigger republishing a sweep for the remaining - // input. - sweepTx2 := ctx.receiveTx() - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 2) - - ctx.backend.mine() - - // Mock a confirmed event. - bumpResultChan1 <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx1, - FeeRate: 10, - Fee: 100, - } - bumpResultChan2 <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx2, - FeeRate: 10, - Fee: 100, - } - - select { - case result := <-spendChan2: - if result.Err != nil { - t.Fatalf("expected successful sweep") - } - case <-time.After(defaultTestTimeout): - t.Fatalf("no result received") - } - - // Restart sweeper again. No action is expected. - ctx.restartSweeper() - - ctx.finish(1) -} - -// TestRestartRemoteSpend asserts that the sweeper picks up sweeping properly -// after a restart with remote spend. -func TestRestartRemoteSpend(t *testing.T) { - ctx := createSweeperTestContext(t) - - // Get testing inputs. - input1 := spendableInputs[0] - input2 := spendableInputs[1] - - // Create a fake sweep tx that spends the second input as the first - // will be spent by the remote. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: input2.OutPoint()}, - }, - } - - // Mock the Broadcast method to succeed. - bumpResultChan := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan, nil).Run(func(args mock.Arguments) { - // Send the first event. - bumpResultChan <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - _, err := ctx.sweeper.SweepInput(input1, defaultFeePref) - require.NoError(t, err) - - // Sweep another input. - _, err = ctx.sweeper.SweepInput(input2, defaultFeePref) - require.NoError(t, err) - - sweepTx := ctx.receiveTx() - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 1) - - // Restart sweeper. - ctx.restartSweeper() - - // Replace the sweep tx with a remote tx spending input 2. - ctx.backend.deleteUnconfirmed(sweepTx.TxHash()) - - remoteTx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: input1.OutPoint()}, - }, - } - err = ctx.backend.publishTransaction(remoteTx) - require.NoError(t, err) - - // Mine remote spending tx. - ctx.backend.mine() - - // Mock the Broadcast method to succeed. - bumpResultChan = make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan, nil).Run(func(args mock.Arguments) { - // Send the first event. - bumpResultChan <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - // Simulate other subsystem (e.g. contract resolver) re-offering input - // 2. - spendChan, err := ctx.sweeper.SweepInput(input2, defaultFeePref) - require.NoError(t, err) - - // Expect sweeper to construct a new tx, because input 1 was spend - // remotely. - sweepTx = ctx.receiveTx() - - ctx.backend.mine() - - // Mock a confirmed event. - bumpResultChan <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx, - FeeRate: 10, - Fee: 100, - } - - ctx.expectResult(spendChan, nil) - - ctx.finish(1) -} - -// TestRestartConfirmed asserts that the sweeper picks up sweeping properly -// after a restart with a confirm of our own sweep tx. -func TestRestartConfirmed(t *testing.T) { - ctx := createSweeperTestContext(t) - - // Sweep input. - input := spendableInputs[0] - - // Mock the Broadcast method to succeed. - bumpResultChan := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: input.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - _, err := ctx.sweeper.SweepInput(input, defaultFeePref) - require.NoError(t, err) - - sweepTx := ctx.receiveTx() - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 1) - - // Restart sweeper. - ctx.restartSweeper() - - // Mine the sweep tx. - ctx.backend.mine() - - // Mock a confirmed event. - bumpResultChan <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx, - FeeRate: 10, - Fee: 100, - } - - // Simulate other subsystem (e.g. contract resolver) re-offering input - // 0. - spendChan, err := ctx.sweeper.SweepInput(input, defaultFeePref) - require.NoError(t, err) - if err != nil { - t.Fatal(err) - } - - // Here we expect again a successful sweep. - ctx.expectResult(spendChan, nil) - - ctx.finish(1) -} - -// TestRetry tests the sweeper retry flow. -func TestRetry(t *testing.T) { - ctx := createSweeperTestContext(t) - - inp0 := spendableInputs[0] - inp1 := spendableInputs[1] - - // Mock the Broadcast method to succeed. - bumpResultChan1 := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan1, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: inp0.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan1 <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - resultChan0, err := ctx.sweeper.SweepInput(inp0, defaultFeePref) - require.NoError(t, err) - - // We expect a sweep to be published. - sweepTx1 := ctx.receiveTx() - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 1) - - // Mock the Broadcast method to succeed on the second sweep. - bumpResultChan2 := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan2, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: inp1.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan2 <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - // Offer a fresh input. - resultChan1, err := ctx.sweeper.SweepInput(inp1, defaultFeePref) - require.NoError(t, err) - - // A single tx is expected to be published. - sweepTx2 := ctx.receiveTx() - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 2) - - ctx.backend.mine() - - // Mock a confirmed event. - bumpResultChan1 <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx1, - FeeRate: 10, - Fee: 100, - } - bumpResultChan2 <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx2, - FeeRate: 10, - Fee: 100, - } - - ctx.expectResult(resultChan0, nil) - ctx.expectResult(resultChan1, nil) - - ctx.finish(1) -} - -// TestDifferentFeePreferences ensures that the sweeper can have different -// transactions for different fee preferences. These transactions should be -// broadcast from highest to lowest fee rate. -func TestDifferentFeePreferences(t *testing.T) { - ctx := createSweeperTestContext(t) - - // Throughout this test, we'll be attempting to sweep three inputs, two - // with the higher fee preference, and the last with the lower. We do - // this to ensure the sweeper can broadcast distinct transactions for - // each sweep with a different fee preference. - lowFeePref := FeeEstimateInfo{ConfTarget: 12} - lowFeeRate := chainfee.SatPerKWeight(5000) - ctx.estimator.blocksToFee[lowFeePref.ConfTarget] = lowFeeRate - - highFeePref := FeeEstimateInfo{ConfTarget: 6} - highFeeRate := chainfee.SatPerKWeight(10000) - ctx.estimator.blocksToFee[highFeePref.ConfTarget] = highFeeRate - - input1 := spendableInputs[0] - input2 := spendableInputs[1] - input3 := spendableInputs[2] - - // Mock the Broadcast method to succeed on the first sweep. - bumpResultChan1 := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan1, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: input1.OutPoint()}, - {PreviousOutPoint: input2.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan1 <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - // Mock the Broadcast method to succeed on the second sweep. - bumpResultChan2 := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan2, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: input3.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan2 <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - resultChan1, err := ctx.sweeper.SweepInput( - input1, Params{Fee: highFeePref}, - ) - require.NoError(t, err) - - resultChan2, err := ctx.sweeper.SweepInput( - input2, Params{Fee: highFeePref}, - ) - require.NoError(t, err) - - resultChan3, err := ctx.sweeper.SweepInput( - input3, Params{Fee: lowFeePref}, - ) - require.NoError(t, err) - - // The first transaction broadcast should be the one spending the - // higher fee rate inputs. - sweepTx1 := ctx.receiveTx() - - // The second should be the one spending the lower fee rate inputs. - sweepTx2 := ctx.receiveTx() - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 2) - - // With the transactions broadcast, we'll mine a block to so that the - // result is delivered to each respective client. - ctx.backend.mine() - - // Mock a confirmed event. - bumpResultChan1 <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx1, - FeeRate: 10, - Fee: 100, - } - bumpResultChan2 <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx2, - FeeRate: 10, - Fee: 100, - } - - resultChans := []chan Result{resultChan1, resultChan2, resultChan3} - for _, resultChan := range resultChans { - ctx.expectResult(resultChan, nil) - } - - ctx.finish(1) -} - -// TestPendingInputs ensures that the sweeper correctly determines the inputs -// pending to be swept. -func TestPendingInputs(t *testing.T) { - ctx := createSweeperTestContext(t) - - // Throughout this test, we'll be attempting to sweep three inputs, two - // with the higher fee preference, and the last with the lower. We do - // this to ensure the sweeper can return all pending inputs, even those - // with different fee preferences. - const ( - lowFeeRate = 5000 - highFeeRate = 10000 - ) - - lowFeePref := FeeEstimateInfo{ - ConfTarget: 12, - } - ctx.estimator.blocksToFee[lowFeePref.ConfTarget] = lowFeeRate - - highFeePref := FeeEstimateInfo{ - ConfTarget: 6, - } - ctx.estimator.blocksToFee[highFeePref.ConfTarget] = highFeeRate - - input1 := spendableInputs[0] - input2 := spendableInputs[1] - input3 := spendableInputs[2] - - // Mock the Broadcast method to succeed on the first sweep. - bumpResultChan1 := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan1, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: input1.OutPoint()}, - {PreviousOutPoint: input2.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan1 <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - // Mock the Broadcast method to succeed on the second sweep. - bumpResultChan2 := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan2, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: input3.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan2 <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - resultChan1, err := ctx.sweeper.SweepInput( - input1, Params{Fee: highFeePref}, - ) - require.NoError(t, err) - - _, err = ctx.sweeper.SweepInput( - input2, Params{Fee: highFeePref}, - ) - require.NoError(t, err) - - resultChan3, err := ctx.sweeper.SweepInput( - input3, Params{Fee: lowFeePref}, - ) - require.NoError(t, err) - - // We should expect to see all inputs pending. - ctx.assertPendingInputs(input1, input2, input3) - - // We should expect to see both sweep transactions broadcast - one for - // the higher feerate, the other for the lower. - sweepTx1 := ctx.receiveTx() - sweepTx2 := ctx.receiveTx() - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 2) - - // Mine these txns, and we should expect to see the results delivered. - ctx.backend.mine() - - // Mock a confirmed event. - bumpResultChan1 <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx1, - FeeRate: 10, - Fee: 100, - } - bumpResultChan2 <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx2, - FeeRate: 10, - Fee: 100, - } - - ctx.expectResult(resultChan1, nil) - ctx.expectResult(resultChan3, nil) - ctx.assertPendingInputs() - - ctx.finish(1) -} - -// TestExclusiveGroup tests the sweeper exclusive group functionality. -func TestExclusiveGroup(t *testing.T) { - ctx := createSweeperTestContext(t) - - input1 := spendableInputs[0] - input2 := spendableInputs[1] - input3 := spendableInputs[2] - - // Mock the Broadcast method to succeed on the first sweep. - bumpResultChan1 := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan1, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: input1.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan1 <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - // Mock the Broadcast method to succeed on the second sweep. - bumpResultChan2 := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan2, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: input2.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan2 <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - // Mock the Broadcast method to succeed on the third sweep. - bumpResultChan3 := make(chan *BumpResult, 1) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan3, nil).Run(func(args mock.Arguments) { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{ - {PreviousOutPoint: input3.OutPoint()}, - }, - } - - // Send the first event. - bumpResultChan3 <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need to - // manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them will - // mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - - // Sweep three inputs in the same exclusive group. - var results []chan Result - for i := 0; i < 3; i++ { - exclusiveGroup := uint64(1) - result, err := ctx.sweeper.SweepInput( - spendableInputs[i], Params{ - Fee: FeeEstimateInfo{ConfTarget: 6}, - ExclusiveGroup: &exclusiveGroup, - }, - ) - require.NoError(t, err) - results = append(results, result) - } - - // We expect all inputs to be published in separate transactions, even - // though they share the same fee preference. - sweepTx1 := ctx.receiveTx() - require.Len(t, sweepTx1.TxIn, 1) - - sweepTx2 := ctx.receiveTx() - sweepTx3 := ctx.receiveTx() - - // Remove all txes except for the one that sweeps the first - // input. This simulates the sweeps being conflicting. - ctx.backend.deleteUnconfirmed(sweepTx2.TxHash()) - ctx.backend.deleteUnconfirmed(sweepTx3.TxHash()) - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 3) - - // Mine the first sweep tx. - ctx.backend.mine() - - // Mock a confirmed event. - bumpResultChan1 <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTx1, - FeeRate: 10, - Fee: 100, - } - bumpResultChan2 <- &BumpResult{ - Event: TxFailed, - Tx: &sweepTx2, - } - bumpResultChan2 <- &BumpResult{ - Event: TxFailed, - Tx: &sweepTx3, - } - - // Expect the first input to be swept by the confirmed sweep tx. - result0 := <-results[0] - if result0.Err != nil { - t.Fatal("expected first input to be swept") - } - - // Expect the other two inputs to return an error. They have no chance - // of confirming. - result1 := <-results[1] - if result1.Err != ErrExclusiveGroupSpend { - t.Fatal("expected second input to be canceled") - } - - result2 := <-results[2] - if result2.Err != ErrExclusiveGroupSpend { - t.Fatal("expected third input to be canceled") - } -} - -type testInput struct { - *input.BaseInput - - locktime *uint32 - reqTxOut *wire.TxOut -} - -func (i *testInput) RequiredLockTime() (uint32, bool) { - if i.locktime != nil { - return *i.locktime, true - } - - return 0, false -} - -func (i *testInput) RequiredTxOut() *wire.TxOut { - return i.reqTxOut -} - -// CraftInputScript is a custom sign method for the testInput type that will -// encode the spending outpoint and the tx input index as part of the returned -// witness. -func (i *testInput) CraftInputScript(_ input.Signer, txn *wire.MsgTx, - hashCache *txscript.TxSigHashes, - prevOutputFetcher txscript.PrevOutputFetcher, - txinIdx int) (*input.Script, error) { - - // We'll encode the outpoint in the witness, so we can assert that the - // expected input was signed at the correct index. - op := i.OutPoint() - return &input.Script{ - Witness: [][]byte{ - // We encode the hash of the outpoint... - op.Hash[:], - // ..the outpoint index... - {byte(op.Index)}, - // ..and finally the tx input index. - {byte(txinIdx)}, - }, - }, nil -} - -// assertSignedIndex goes through all inputs to the tx and checks that all -// testInputs have witnesses corresponding to the outpoints they are spending, -// and are signed at the correct tx input index. All found testInputs are -// returned such that we can sum up and sanity check that all testInputs were -// part of the sweep. -func assertSignedIndex(t *testing.T, tx *wire.MsgTx, - testInputs map[wire.OutPoint]*testInput) map[wire.OutPoint]struct{} { - - found := make(map[wire.OutPoint]struct{}) - for idx, txIn := range tx.TxIn { - op := txIn.PreviousOutPoint - - // Not a testInput, it won't have the test encoding we require - // to check outpoint and index. - if _, ok := testInputs[op]; !ok { - continue - } - - if _, ok := found[op]; ok { - t.Fatalf("input already used") - } - - // Check it was signes spending the correct outpoint, and at - // the expected tx input index. - require.Equal(t, txIn.Witness[0], op.Hash[:]) - require.Equal(t, txIn.Witness[1], []byte{byte(op.Index)}) - require.Equal(t, txIn.Witness[2], []byte{byte(idx)}) - found[op] = struct{}{} - } - - return found -} - -// TestLockTimes checks that the sweeper properly groups inputs requiring the -// same locktime together into sweep transactions. -func TestLockTimes(t *testing.T) { - ctx := createSweeperTestContext(t) - - // We increase the number of max inputs to a tx so that won't - // impact our test. - ctx.sweeper.cfg.MaxInputsPerTx = 100 - - // We also need to update the aggregator about this new config. - ctx.sweeper.cfg.Aggregator = NewSimpleUtxoAggregator( - ctx.estimator, DefaultMaxFeeRate.FeePerKWeight(), 100, - ) - - // We will set up the lock times in such a way that we expect the - // sweeper to divide the inputs into 4 diffeerent transactions. - const numSweeps = 4 - - // Sweep 8 inputs, using 4 different lock times. - var ( - results []chan Result - inputs = make(map[wire.OutPoint]input.Input) - clusters = make(map[uint32][]input.Input) - bumpResultChans = make([]chan *BumpResult, 0, 4) - ) - for i := 0; i < numSweeps*2; i++ { - lt := uint32(10 + (i % numSweeps)) - inp := &testInput{ - BaseInput: spendableInputs[i], - locktime: <, - } - - op := inp.OutPoint() - inputs[op] = inp - - cluster, ok := clusters[lt] - if !ok { - cluster = make([]input.Input, 0) - } - cluster = append(cluster, inp) - clusters[lt] = cluster - } - - for i := 0; i < 3; i++ { - inp := spendableInputs[i+numSweeps*2] - inputs[inp.OutPoint()] = inp - - lt := uint32(10 + (i % numSweeps)) - clusters[lt] = append(clusters[lt], inp) - } - - for lt, cluster := range clusters { - // Create a fake sweep tx. - tx := &wire.MsgTx{ - TxIn: []*wire.TxIn{}, - LockTime: lt, - } - - // Append the inputs. - for _, inp := range cluster { - txIn := &wire.TxIn{ - PreviousOutPoint: inp.OutPoint(), - } - tx.TxIn = append(tx.TxIn, txIn) - } - - // Mock the Broadcast method to succeed on current sweep. - bumpResultChan := make(chan *BumpResult, 1) - bumpResultChans = append(bumpResultChans, bumpResultChan) - ctx.publisher.On("Broadcast", mock.Anything).Return( - bumpResultChan, nil).Run(func(args mock.Arguments) { - // Send the first event. - bumpResultChan <- &BumpResult{ - Event: TxPublished, - Tx: tx, - } - - // Due to a mix of new and old test frameworks, we need - // to manually call the method to get the test to pass. - // - // TODO(yy): remove the test context and replace them - // will mocks. - err := ctx.backend.PublishTransaction(tx, "") - require.NoError(t, err) - }).Once() - } - - // Make all the sweeps. - for _, inp := range inputs { - result, err := ctx.sweeper.SweepInput( - inp, Params{ - Fee: FeeEstimateInfo{ConfTarget: 6}, - }, - ) - require.NoError(t, err) - - results = append(results, result) - } - - // Check the sweeps transactions, ensuring all inputs are there, and - // all the locktimes are satisfied. - sweepTxes := make([]wire.MsgTx, 0, numSweeps) - for i := 0; i < numSweeps; i++ { - sweepTx := ctx.receiveTx() - sweepTxes = append(sweepTxes, sweepTx) - - for _, txIn := range sweepTx.TxIn { - op := txIn.PreviousOutPoint - inp, ok := inputs[op] - require.True(t, ok) - - delete(inputs, op) - - // If this input had a required locktime, ensure the tx - // has that set correctly. - lt, ok := inp.RequiredLockTime() - if !ok { - continue - } - - require.EqualValues(t, lt, sweepTx.LockTime) - } - } - - // Wait until the sweep tx has been saved to db. - assertNumSweeps(t, ctx.sweeper, 4) - - // Mine the sweeps. - ctx.backend.mine() - - for i, bumpResultChan := range bumpResultChans { - // Mock a confirmed event. - bumpResultChan <- &BumpResult{ - Event: TxConfirmed, - Tx: &sweepTxes[i], - FeeRate: 10, - Fee: 100, - } - } - - // The should be no inputs not foud in any of the sweeps. - require.Empty(t, inputs) - - // Results should all come back. - for i, resultChan := range results { - select { - case result := <-resultChan: - require.NoError(t, result.Err) - case <-time.After(1 * time.Second): - t.Fatalf("result %v did not come back", i) - } - } -} - -// TestSweeperShutdownHandling tests that we notify callers when the sweeper -// cannot handle requests since it's in the process of shutting down. -func TestSweeperShutdownHandling(t *testing.T) { - ctx := createSweeperTestContext(t) - - // Make the backing notifier break down. This is what happens during - // lnd shut down, since the notifier is stopped before the sweeper. - require.Len(t, ctx.notifier.epochChan, 1) - for epochChan := range ctx.notifier.epochChan { - close(epochChan) - } - - // Give the collector some time to exit. - time.Sleep(50 * time.Millisecond) - - // Now trying to sweep inputs should return an error on the error - // channel. - resultChan, err := ctx.sweeper.SweepInput( - spendableInputs[0], defaultFeePref, - ) - require.NoError(t, err) - - select { - case res := <-resultChan: - require.Equal(t, ErrSweeperShuttingDown, res.Err) - - case <-time.After(defaultTestTimeout): - t.Fatalf("no result arrived") - } - - // Stop the sweeper properly. - err = ctx.sweeper.Stop() - require.NoError(t, err) - - // Now attempting to sweep an input should error out immediately. - _, err = ctx.sweeper.SweepInput( - spendableInputs[0], defaultFeePref, - ) - require.Error(t, err) -} - // TestMarkInputsPendingPublish checks that given a list of inputs with // different states, only the non-terminal state will be marked as `Published`. func TestMarkInputsPendingPublish(t *testing.T) { diff --git a/sweep/tx_input_set.go b/sweep/tx_input_set.go index 5d7b3f797..cfa4a0ba4 100644 --- a/sweep/tx_input_set.go +++ b/sweep/tx_input_set.go @@ -14,23 +14,6 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) -// addConstraints defines the constraints to apply when adding an input. -type addConstraints uint8 - -const ( - // constraintsRegular is for regular input sweeps that should have a positive - // yield. - constraintsRegular addConstraints = iota - - // constraintsWallet is for wallet inputs that are only added to bring up the tx - // output value. - constraintsWallet - - // constraintsForce is for inputs that should be swept even with a negative - // yield at the set fee rate. - constraintsForce -) - var ( // ErrNotEnoughInputs is returned when there are not enough wallet // inputs to construct a non-dust change output for an input set. @@ -83,443 +66,6 @@ type InputSet interface { StartingFeeRate() fn.Option[chainfee.SatPerKWeight] } -type txInputSetState struct { - // feeRate is the fee rate to use for the sweep transaction. - feeRate chainfee.SatPerKWeight - - // maxFeeRate is the max allowed fee rate configured by the user. - maxFeeRate chainfee.SatPerKWeight - - // inputTotal is the total value of all inputs. - inputTotal btcutil.Amount - - // requiredOutput is the sum of the outputs committed to by the inputs. - requiredOutput btcutil.Amount - - // changeOutput is the value of the change output. This will be what is - // left over after subtracting the requiredOutput and the tx fee from - // the inputTotal. - // - // NOTE: This might be below the dust limit, or even negative since it - // is the change remaining in csse we pay the fee for a change output. - changeOutput btcutil.Amount - - // inputs is the set of tx inputs. - inputs []input.Input - - // walletInputTotal is the total value of inputs coming from the wallet. - walletInputTotal btcutil.Amount - - // force indicates that this set must be swept even if the total yield - // is negative. - force bool -} - -// weightEstimate is the (worst case) tx weight with the current set of -// inputs. It takes a parameter whether to add a change output or not. -func (t *txInputSetState) weightEstimate(change bool) *weightEstimator { - weightEstimate := newWeightEstimator(t.feeRate, t.maxFeeRate) - for _, i := range t.inputs { - // Can ignore error, because it has already been checked when - // calculating the yields. - _ = weightEstimate.add(i) - - r := i.RequiredTxOut() - if r != nil { - weightEstimate.addOutput(r) - } - } - - // Add a change output to the weight estimate if requested. - if change { - weightEstimate.addP2TROutput() - } - - return weightEstimate -} - -// totalOutput is the total amount left for us after paying fees. -// -// NOTE: This might be dust. -func (t *txInputSetState) totalOutput() btcutil.Amount { - return t.requiredOutput + t.changeOutput -} - -func (t *txInputSetState) clone() txInputSetState { - s := txInputSetState{ - feeRate: t.feeRate, - inputTotal: t.inputTotal, - changeOutput: t.changeOutput, - requiredOutput: t.requiredOutput, - walletInputTotal: t.walletInputTotal, - force: t.force, - inputs: make([]input.Input, len(t.inputs)), - } - copy(s.inputs, t.inputs) - - return s -} - -// txInputSet is an object that accumulates tx inputs and keeps running counters -// on various properties of the tx. -type txInputSet struct { - txInputSetState - - // maxInputs is the maximum number of inputs that will be accepted in - // the set. - maxInputs uint32 -} - -// Compile-time constraint to ensure txInputSet implements InputSet. -var _ InputSet = (*txInputSet)(nil) - -// newTxInputSet constructs a new, empty input set. -func newTxInputSet(feePerKW, maxFeeRate chainfee.SatPerKWeight, - maxInputs uint32) *txInputSet { - - state := txInputSetState{ - feeRate: feePerKW, - maxFeeRate: maxFeeRate, - } - - b := txInputSet{ - maxInputs: maxInputs, - txInputSetState: state, - } - - return &b -} - -// Inputs returns the inputs that should be used to create a tx. -func (t *txInputSet) Inputs() []input.Input { - return t.inputs -} - -// Budget gives the total amount that can be used as fees by this input set. -// -// NOTE: this field is only used for `BudgetInputSet`. -func (t *txInputSet) Budget() btcutil.Amount { - return t.totalOutput() -} - -// DeadlineHeight gives the block height that this set must be confirmed by. -// -// NOTE: this field is only used for `BudgetInputSet`. -func (t *txInputSet) DeadlineHeight() int32 { - return 0 -} - -// StartingFeeRate returns the max starting fee rate found in the inputs. -// -// NOTE: this field is only used for `BudgetInputSet`. -func (t *txInputSet) StartingFeeRate() fn.Option[chainfee.SatPerKWeight] { - return fn.None[chainfee.SatPerKWeight]() -} - -// NeedWalletInput returns true if the input set needs more wallet inputs. -func (t *txInputSet) NeedWalletInput() bool { - return !t.enoughInput() -} - -// enoughInput returns true if we've accumulated enough inputs to pay the fees -// and have at least one output that meets the dust limit. -func (t *txInputSet) enoughInput() bool { - // If we have a change output above dust, then we certainly have enough - // inputs to the transaction. - if t.changeOutput >= lnwallet.DustLimitForSize(input.P2TRSize) { - return true - } - - // We did not have enough input for a change output. Check if we have - // enough input to pay the fees for a transaction with no change - // output. - fee := t.weightEstimate(false).feeWithParent() - if t.inputTotal < t.requiredOutput+fee { - return false - } - - // We could pay the fees, but we still need at least one output to be - // above the dust limit for the tx to be valid (we assume that these - // required outputs only get added if they are above dust) - for _, inp := range t.inputs { - if inp.RequiredTxOut() != nil { - return true - } - } - - return false -} - -// add adds a new input to the set. It returns a bool indicating whether the -// input was added to the set. An input is rejected if it decreases the tx -// output value after paying fees. -func (t *txInputSet) addToState(inp input.Input, - constraints addConstraints) *txInputSetState { - - // Stop if max inputs is reached. Do not count additional wallet inputs, - // because we don't know in advance how many we may need. - if constraints != constraintsWallet && - uint32(len(t.inputs)) >= t.maxInputs { - - return nil - } - - // If the input comes with a required tx out that is below dust, we - // won't add it. - // - // NOTE: only HtlcSecondLevelAnchorInput returns non-nil RequiredTxOut. - reqOut := inp.RequiredTxOut() - if reqOut != nil { - // Fetch the dust limit for this output. - dustLimit := lnwallet.DustLimitForSize(len(reqOut.PkScript)) - if btcutil.Amount(reqOut.Value) < dustLimit { - log.Errorf("Rejected input=%v due to dust required "+ - "output=%v, limit=%v", inp, reqOut.Value, - dustLimit) - - // TODO(yy): we should not return here for force - // sweeps. This means when sending sweeping request, - // one must be careful to not create dust outputs. In - // an extreme rare case, where the - // minRelayTxFee/discardfee is increased when sending - // the request, what's considered non-dust at the - // caller side will be dust here, causing a force sweep - // to fail. - return nil - } - } - - // Clone the current set state. - newSet := t.clone() - - // Add the new input. - newSet.inputs = append(newSet.inputs, inp) - - // Add the value of the new input. - value := btcutil.Amount(inp.SignDesc().Output.Value) - newSet.inputTotal += value - - // Recalculate the tx fee. - fee := newSet.weightEstimate(true).feeWithParent() - - // Calculate the new output value. - if reqOut != nil { - newSet.requiredOutput += btcutil.Amount(reqOut.Value) - } - - // NOTE: `changeOutput` could be negative here if this input is using - // constraintsForce. - newSet.changeOutput = newSet.inputTotal - newSet.requiredOutput - fee - - // Calculate the yield of this input from the change in total tx output - // value. - inputYield := newSet.totalOutput() - t.totalOutput() - - switch constraints { - // Don't sweep inputs that cost us more to sweep than they give us. - case constraintsRegular: - if inputYield <= 0 { - log.Debugf("Rejected regular input=%v due to negative "+ - "yield=%v", value, inputYield) - - return nil - } - - // For force adds, no further constraints apply. - // - // NOTE: because the inputs are sorted with force sweeps being placed - // at the start of the list, we should never see an input with - // constraintsForce come after an input with constraintsRegular. In - // other words, though we may have negative `changeOutput` from - // including force sweeps, `inputYield` should always increase when - // adding regular inputs. - case constraintsForce: - newSet.force = true - - // We are attaching a wallet input to raise the tx output value above - // the dust limit. - case constraintsWallet: - // Skip this wallet input if adding it would lower the output - // value. - // - // TODO(yy): change to inputYield < 0 to allow sweeping for - // UTXO aggregation only? - if inputYield <= 0 { - log.Debugf("Rejected wallet input=%v due to negative "+ - "yield=%v", value, inputYield) - - return nil - } - - // Calculate the total value that we spend in this tx from the - // wallet if we'd add this wallet input. - newSet.walletInputTotal += value - - // In any case, we don't want to lose money by sweeping. If we - // don't get more out of the tx than we put in ourselves, do not - // add this wallet input. If there is at least one force sweep - // in the set, this does no longer apply. - // - // We should only add wallet inputs to get the tx output value - // above the dust limit, otherwise we'd only burn into fees. - // This is guarded by tryAddWalletInputsIfNeeded. - // - // TODO(joostjager): Possibly require a max ratio between the - // value of the wallet input and what we get out of this - // transaction. To prevent attaching and locking a big utxo for - // very little benefit. - if newSet.force { - break - } - - // TODO(yy): change from `>=` to `>` to allow non-negative - // sweeping - we won't gain more coins from this sweep, but - // aggregating small UTXOs. - if newSet.walletInputTotal >= newSet.totalOutput() { - // TODO(yy): further check this case as it seems we can - // never reach here because it'd mean `inputYield` is - // already <= 0? - log.Debugf("Rejecting wallet input of %v, because it "+ - "would make a negative yielding transaction "+ - "(%v)", value, - newSet.totalOutput()-newSet.walletInputTotal) - - return nil - } - } - - return &newSet -} - -// add adds a new input to the set. It returns a bool indicating whether the -// input was added to the set. An input is rejected if it decreases the tx -// output value after paying fees. -func (t *txInputSet) add(input input.Input, constraints addConstraints) bool { - newState := t.addToState(input, constraints) - if newState == nil { - return false - } - - t.txInputSetState = *newState - - return true -} - -// addPositiveYieldInputs adds sweepableInputs that have a positive yield to the -// input set. This function assumes that the list of inputs is sorted descending -// by yield. -// -// 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 (t *txInputSet) addPositiveYieldInputs(sweepableInputs []*SweeperInput) { - for i, inp := range sweepableInputs { - // Apply relaxed constraints for force sweeps. - constraints := constraintsRegular - if inp.parameters().Immediate { - constraints = constraintsForce - } - - // Try to add the input to the transaction. If that doesn't - // succeed because it wouldn't increase the output value, - // return. Assuming inputs are sorted by yield, any further - // inputs wouldn't increase the output value either. - if !t.add(inp, constraints) { - var rem []input.Input - for j := i; j < len(sweepableInputs); j++ { - rem = append(rem, sweepableInputs[j]) - } - log.Debugf("%d negative yield inputs not added to "+ - "input set: %v", len(rem), - inputTypeSummary(rem)) - return - } - - log.Debugf("Added positive yield input %v to input set", - inputTypeSummary([]input.Input{inp})) - } - - // We managed to add all inputs to the set. -} - -// AddWalletInputs adds wallet inputs to the set until a non-dust output can be -// made. This non-dust output is either a change output or a required output. -// Return an error if there are not enough wallet inputs. -func (t *txInputSet) AddWalletInputs(wallet Wallet) error { - // Check the current output value and add wallet utxos if needed to - // push the output value to the lower limit. - if err := t.tryAddWalletInputsIfNeeded(wallet); err != nil { - return err - } - - // 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 an even lower output - // value. - if !t.enoughInput() { - // The change output is always a p2tr here. - dl := lnwallet.DustLimitForSize(input.P2TRSize) - log.Debugf("Input set value %v (required=%v, change=%v) "+ - "below dust limit of %v", t.totalOutput(), - t.requiredOutput, t.changeOutput, dl) - - return ErrNotEnoughInputs - } - - return nil -} - -// tryAddWalletInputsIfNeeded retrieves utxos from the wallet and tries adding -// as many as required to bring the tx output value above the given minimum. -func (t *txInputSet) tryAddWalletInputsIfNeeded(wallet Wallet) error { - // If we've already have enough to pay the transaction fees and have at - // least one output materialize, no action is needed. - if t.enoughInput() { - return nil - } - - // Retrieve wallet utxos. Only consider confirmed utxos to prevent - // problems around RBF rules for unconfirmed inputs. This currently - // ignores the configured coin selection strategy. - utxos, err := wallet.ListUnspentWitnessFromDefaultAccount( - 1, math.MaxInt32, - ) - if err != nil { - return err - } - - // Sort the UTXOs by putting smaller values at the start of the slice - // to avoid locking large UTXO for sweeping. - // - // TODO(yy): add more choices to CoinSelectionStrategy and use the - // configured value here. - sort.Slice(utxos, func(i, j int) bool { - return utxos[i].Value < utxos[j].Value - }) - - for _, utxo := range utxos { - input, err := createWalletTxInput(utxo) - if err != nil { - return err - } - - // If the wallet input isn't positively-yielding at this fee - // rate, skip it. - if !t.add(input, constraintsWallet) { - continue - } - - // Return if we've reached the minimum output amount. - if t.enoughInput() { - return nil - } - } - - // We were not able to reach the minimum output amount. - return nil -} - // createWalletTxInput converts a wallet utxo into an object that can be added // to the other inputs to sweep. func createWalletTxInput(utxo *lnwallet.Utxo) (input.Input, error) { diff --git a/sweep/tx_input_set_test.go b/sweep/tx_input_set_test.go index 2cf97dd0b..b6a87b378 100644 --- a/sweep/tx_input_set_test.go +++ b/sweep/tx_input_set_test.go @@ -14,237 +14,12 @@ import ( "github.com/stretchr/testify/require" ) -// TestTxInputSet tests adding various sized inputs to the set. -func TestTxInputSet(t *testing.T) { - const ( - feeRate = 1000 - maxInputs = 10 - ) - set := newTxInputSet(feeRate, 0, maxInputs) - - // Create a 300 sat input. The fee to sweep this input to a P2WKH output - // is 439 sats. That means that this input yields -139 sats and we - // expect it not to be added. - if set.add(createP2WKHInput(300), constraintsRegular) { - t.Fatal("expected add of negatively yielding input to fail") - } - - // A 700 sat input should be accepted into the set, because it yields - // positively. - if !set.add(createP2WKHInput(700), constraintsRegular) { - t.Fatal("expected add of positively yielding input to succeed") - } - - fee := set.weightEstimate(true).feeWithParent() - require.Equal(t, btcutil.Amount(487), fee) - - // The tx output should now be 700-487 = 213 sats. The dust limit isn't - // reached yet. - if set.totalOutput() != 213 { - t.Fatal("unexpected output value") - } - if set.enoughInput() { - t.Fatal("expected dust limit not yet to be reached") - } - - // Add a 1000 sat input. This increases the tx fee to 760 sats. The tx - // output should now be 1000+700 - 760 = 940 sats. - if !set.add(createP2WKHInput(1000), constraintsRegular) { - t.Fatal("expected add of positively yielding input to succeed") - } - if set.totalOutput() != 940 { - t.Fatal("unexpected output value") - } - if !set.enoughInput() { - t.Fatal("expected dust limit to be reached") - } -} - -// TestTxInputSetFromWallet tests adding a wallet input to a TxInputSet to reach -// the dust limit. -func TestTxInputSetFromWallet(t *testing.T) { - const ( - feeRate = 500 - maxInputs = 10 - ) - - wallet := &mockWallet{} - set := newTxInputSet(feeRate, 0, maxInputs) - - // Add a 500 sat input to the set. It yields positively, but doesn't - // reach the output dust limit. - if !set.add(createP2WKHInput(500), constraintsRegular) { - t.Fatal("expected add of positively yielding input to succeed") - } - if set.enoughInput() { - t.Fatal("expected dust limit not yet to be reached") - } - - // Expect that adding a negative yield input fails. - if set.add(createP2WKHInput(50), constraintsRegular) { - t.Fatal("expected negative yield input add to fail") - } - - // Force add the negative yield input. It should succeed. - if !set.add(createP2WKHInput(50), constraintsForce) { - t.Fatal("expected forced add to succeed") - } - - err := set.AddWalletInputs(wallet) - if err != nil { - t.Fatal(err) - } - - if !set.enoughInput() { - t.Fatal("expected dust limit to be reached") - } -} - // createP2WKHInput returns a P2WKH test input with the specified amount. func createP2WKHInput(amt btcutil.Amount) input.Input { input := createTestInput(int64(amt), input.WitnessKeyHash) return &input } -type mockWallet struct { - Wallet -} - -func (m *mockWallet) ListUnspentWitnessFromDefaultAccount(minConfs, maxConfs int32) ( - []*lnwallet.Utxo, error) { - - return []*lnwallet.Utxo{ - { - AddressType: lnwallet.WitnessPubKey, - Value: 10000, - }, - }, nil -} - -type reqInput struct { - input.Input - - txOut *wire.TxOut -} - -func (r *reqInput) RequiredTxOut() *wire.TxOut { - return r.txOut -} - -// TestTxInputSetRequiredOutput tests that the tx input set behaves as expected -// when we add inputs that have required tx outs. -func TestTxInputSetRequiredOutput(t *testing.T) { - const ( - feeRate = 1000 - maxInputs = 10 - ) - set := newTxInputSet(feeRate, 0, maxInputs) - - // Attempt to add an input with a required txout below the dust limit. - // This should fail since we cannot trim such outputs. - inp := &reqInput{ - Input: createP2WKHInput(500), - txOut: &wire.TxOut{ - Value: 500, - PkScript: make([]byte, input.P2PKHSize), - }, - } - require.False(t, set.add(inp, constraintsRegular), - "expected adding dust required tx out to fail") - - // Create a 1000 sat input that also has a required TxOut of 1000 sat. - // The fee to sweep this input to a P2WKH output is 439 sats. - inp = &reqInput{ - Input: createP2WKHInput(1000), - txOut: &wire.TxOut{ - Value: 1000, - PkScript: make([]byte, input.P2WPKHSize), - }, - } - require.True(t, set.add(inp, constraintsRegular), "failed adding input") - - // The fee needed to pay for this input and output should be 439 sats. - fee := set.weightEstimate(false).feeWithParent() - require.Equal(t, btcutil.Amount(439), fee) - - // Since the tx set currently pays no fees, we expect the current - // change to actually be negative, since this is what it would cost us - // in fees to add a change output. - feeWithChange := set.weightEstimate(true).feeWithParent() - if set.changeOutput != -feeWithChange { - t.Fatalf("expected negative change of %v, had %v", - -feeWithChange, set.changeOutput) - } - - // This should also be reflected by not having enough input. - require.False(t, set.enoughInput()) - - // Get a weight estimate without change output, and add an additional - // input to it. - dummyInput := createP2WKHInput(1000) - weight := set.weightEstimate(false) - require.NoError(t, weight.add(dummyInput)) - - // Now we add a an input that is large enough to pay the fee for the - // transaction without a change output, but not large enough to afford - // adding a change output. - extraInput1 := weight.feeWithParent() + 100 - require.True(t, set.add( - createP2WKHInput(extraInput1), constraintsRegular, - ), "expected add of positively yielding input to succeed") - - // The change should be negative, since we would have to add a change - // output, which we cannot yet afford. - if set.changeOutput >= 0 { - t.Fatal("expected change to be negaitve") - } - - // Even though we cannot afford a change output, the tx set is valid, - // since we can pay the fees without the change output. - require.True(t, set.enoughInput()) - - // Get another weight estimate, this time with a change output, and - // figure out how much we must add to afford a change output. - weight = set.weightEstimate(true) - require.NoError(t, weight.add(dummyInput)) - - // We add what is left to reach this value. - extraInput2 := weight.feeWithParent() - extraInput1 + 100 - - // Add this input, which should result in the change now being 100 sats. - require.True(t, set.add( - createP2WKHInput(extraInput2), constraintsRegular, - )) - - // The change should be 100, since this is what is left after paying - // fees in case of a change output. - change := set.changeOutput - if change != 100 { - t.Fatalf("expected change be 100, was %v", change) - } - - // Even though the change output is dust, we have enough for fees, and - // we have an output, so it should be considered enough to craft a - // valid sweep transaction. - require.True(t, set.enoughInput()) - - // Finally we add an input that should push the change output above the - // dust limit. - weight = set.weightEstimate(true) - require.NoError(t, weight.add(dummyInput)) - - // We expect the change to everything that is left after paying the tx - // fee. - extraInput3 := weight.feeWithParent() - extraInput1 - extraInput2 + 1000 - require.True(t, set.add(createP2WKHInput(extraInput3), constraintsRegular)) - - change = set.changeOutput - if change != 1000 { - t.Fatalf("expected change to be %v, had %v", 1000, change) - } - require.True(t, set.enoughInput()) -} - // TestNewBudgetInputSet checks `NewBudgetInputSet` correctly validates the // supplied inputs and returns the error. func TestNewBudgetInputSet(t *testing.T) {