diff --git a/breacharbiter.go b/breacharbiter.go index 865e67ce..b504d1b8 100644 --- a/breacharbiter.go +++ b/breacharbiter.go @@ -914,6 +914,11 @@ func (bo *breachedOutput) HeightHint() uint32 { return bo.confHeight } +// UnconfParent returns information about a possibly unconfirmed parent tx. +func (bo *breachedOutput) UnconfParent() *input.TxInfo { + return nil +} + // Add compile-time constraint ensuring breachedOutput implements the Input // interface. var _ input.Input = (*breachedOutput)(nil) diff --git a/contractcourt/anchor_resolver.go b/contractcourt/anchor_resolver.go index bc9b251d..ac67173f 100644 --- a/contractcourt/anchor_resolver.go +++ b/contractcourt/anchor_resolver.go @@ -99,6 +99,7 @@ func (c *anchorResolver) Resolve() (ContractResolver, error) { input.CommitmentAnchor, &c.anchorSignDescriptor, c.broadcastHeight, + nil, ) resultChan, err := c.Sweeper.SweepInput( diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 33857c9f..46bb61ba 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -1073,6 +1073,7 @@ func (c *ChannelArbitrator) sweepAnchors(anchors []*lnwallet.AnchorResolution, input.CommitmentAnchor, &anchor.AnchorSignDescriptor, heightHint, + nil, ) // Sweep anchor output with the minimum fee rate. This usually diff --git a/input/input.go b/input/input.go index 38a1651e..2e3a71c0 100644 --- a/input/input.go +++ b/input/input.go @@ -3,6 +3,7 @@ package input import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" ) // Input represents an abstract UTXO which is to be spent using a sweeping @@ -41,6 +42,19 @@ type Input interface { // HeightHint returns the minimum height at which a confirmed spending // tx can occur. HeightHint() uint32 + + // UnconfParent returns information about a possibly unconfirmed parent + // tx. + UnconfParent() *TxInfo +} + +// TxInfo describes properties of a parent tx that are relevant for CPFP. +type TxInfo struct { + // Fee is the fee of the tx. + Fee btcutil.Amount + + // Weight is the weight of the tx. + Weight int64 } type inputKit struct { @@ -49,6 +63,10 @@ type inputKit struct { signDesc SignDescriptor heightHint uint32 blockToMaturity uint32 + + // unconfParent contains information about a potential unconfirmed + // parent transaction. + unconfParent *TxInfo } // OutPoint returns the breached output's identifier that is to be included as @@ -82,6 +100,11 @@ func (i *inputKit) BlocksToMaturity() uint32 { return i.blockToMaturity } +// Cpfp returns information about a possibly unconfirmed parent tx. +func (i *inputKit) UnconfParent() *TxInfo { + return i.unconfParent +} + // BaseInput contains all the information needed to sweep a basic output // (CSV/CLTV/no time lock) type BaseInput struct { @@ -91,14 +114,16 @@ type BaseInput struct { // MakeBaseInput assembles a new BaseInput that can be used to construct a // sweep transaction. func MakeBaseInput(outpoint *wire.OutPoint, witnessType WitnessType, - signDescriptor *SignDescriptor, heightHint uint32) BaseInput { + signDescriptor *SignDescriptor, heightHint uint32, + unconfParent *TxInfo) BaseInput { return BaseInput{ inputKit{ - outpoint: *outpoint, - witnessType: witnessType, - signDesc: *signDescriptor, - heightHint: heightHint, + outpoint: *outpoint, + witnessType: witnessType, + signDesc: *signDescriptor, + heightHint: heightHint, + unconfParent: unconfParent, }, } } @@ -109,7 +134,7 @@ func NewBaseInput(outpoint *wire.OutPoint, witnessType WitnessType, signDescriptor *SignDescriptor, heightHint uint32) *BaseInput { input := MakeBaseInput( - outpoint, witnessType, signDescriptor, heightHint, + outpoint, witnessType, signDescriptor, heightHint, nil, ) return &input diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 68698f5c..501ed078 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -505,8 +505,11 @@ func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch) { log.Debugf("Already pending input %v received", outpoint) - // Update sweep parameters. + // Update input details and sweep parameters. + // The re-offered input details may contain a + // change to the unconfirmed parent tx info. pendInput.params = input.params + pendInput.Input = input.input // Add additional result channel to signal // spend of this input. @@ -763,6 +766,26 @@ func (s *UtxoSweeper) clusterBySweepFeeRate() []inputCluster { 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 diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index d7b6ab13..2f71c35b 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -17,6 +17,7 @@ import ( "github.com/lightningnetwork/lnd/lntest/mock" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/stretchr/testify/require" ) var ( @@ -77,6 +78,7 @@ func createTestInput(value int64, witnessType input.WitnessType) input.BaseInput }, }, 0, + nil, ) testInputCount++ @@ -176,6 +178,18 @@ func (ctx *sweeperTestContext) tick() { } } +// assertNoTick asserts that the sweeper does not wait for a tick. +func (ctx *sweeperTestContext) assertNoTick() { + ctx.t.Helper() + + select { + case <-ctx.timeoutChan: + ctx.t.Fatal("unexpected tick") + + case <-time.After(processingDelay): + } +} + func (ctx *sweeperTestContext) assertNoNewTimer() { select { case <-ctx.timeoutChan: @@ -337,9 +351,10 @@ func assertTxFeeRate(t *testing.T, tx *wire.MsgTx, outputAmt := tx.TxOut[0].Value fee := btcutil.Amount(inputAmt - outputAmt) - _, txWeight := getWeightEstimate(inputs) + _, estimator := getWeightEstimate(inputs, 0) + txWeight := estimator.weight() - expectedFee := expectedFeeRate.FeeForWeight(txWeight) + expectedFee := expectedFeeRate.FeeForWeight(int64(txWeight)) if fee != expectedFee { t.Fatalf("expected fee rate %v results in %v fee, got %v fee", expectedFeeRate, expectedFee, fee) @@ -1293,3 +1308,71 @@ func TestExclusiveGroup(t *testing.T) { t.Fatal("expected third input to be canceled") } } + +// TestCpfp tests that the sweeper spends cpfp inputs at a fee rate that exceeds +// the parent tx fee rate. +func TestCpfp(t *testing.T) { + ctx := createSweeperTestContext(t) + + ctx.estimator.updateFees(1000, chainfee.FeePerKwFloor) + + // Offer an input with an unconfirmed parent tx to the sweeper. The + // parent tx pays 3000 sat/kw. + hash := chainhash.Hash{1} + input := input.MakeBaseInput( + &wire.OutPoint{Hash: hash}, + input.CommitmentTimeLock, + &input.SignDescriptor{ + Output: &wire.TxOut{ + Value: 330, + }, + KeyDesc: keychain.KeyDescriptor{ + PubKey: testPubKey, + }, + }, + 0, + &input.TxInfo{ + Weight: 300, + Fee: 900, + }, + ) + + feePref := FeePreference{ConfTarget: 6} + result, err := ctx.sweeper.SweepInput( + &input, Params{Fee: feePref, Force: true}, + ) + require.NoError(t, err) + + // Because we sweep at 1000 sat/kw, the parent cannot be paid for. We + // expect the sweeper to remain idle. + ctx.assertNoTick() + + // Increase the fee estimate to above the parent tx fee rate. + ctx.estimator.updateFees(5000, chainfee.FeePerKwFloor) + + // Signal a new block. This is a trigger for the sweeper to refresh fee + // estimates. + ctx.notifier.NotifyEpoch(1000) + + // Now we do expect a sweep transaction to be published with our input + // and an attached wallet utxo. + ctx.tick() + tx := ctx.receiveTx() + require.Len(t, tx.TxIn, 2) + require.Len(t, tx.TxOut, 1) + + // As inputs we have 10000 sats from the wallet and 330 sats from the + // cpfp input. The sweep tx is weight expected to be 759 units. There is + // an additional 300 weight units from the parent to include in the + // package, making a total of 1059. At 5000 sat/kw, the required fee for + // the package is 5295 sats. The parent already paid 900 sats, so there + // is 4395 sat remaining to be paid. The expected output value is + // therefore: 10000 + 330 - 4395 = 5935. + require.Equal(t, int64(5935), tx.TxOut[0].Value) + + // Mine the tx and assert that the result is passed back. + ctx.backend.mine() + ctx.expectResult(result, nil) + + ctx.finish(1) +} diff --git a/sweep/test_utils.go b/sweep/test_utils.go index 2fd916c8..5e0c6762 100644 --- a/sweep/test_utils.go +++ b/sweep/test_utils.go @@ -13,6 +13,7 @@ import ( var ( defaultTestTimeout = 5 * time.Second + processingDelay = 1 * time.Second mockChainHash, _ = chainhash.NewHashFromStr("00aabbccddeeff") mockChainHeight = int32(100) ) diff --git a/sweep/tx_input_set.go b/sweep/tx_input_set.go index 47d18ce4..70836900 100644 --- a/sweep/tx_input_set.go +++ b/sweep/tx_input_set.go @@ -71,9 +71,6 @@ func (t *txInputSetState) clone() txInputSetState { type txInputSet struct { txInputSetState - // feePerKW is the fee rate used to calculate the tx fee. - feePerKW chainfee.SatPerKWeight - // dustLimit is the minimum output value of the tx. dustLimit btcutil.Amount @@ -96,11 +93,10 @@ func newTxInputSet(wallet Wallet, feePerKW, ) state := txInputSetState{ - weightEstimate: newWeightEstimator(), + weightEstimate: newWeightEstimator(feePerKW), } b := txInputSet{ - feePerKW: feePerKW, dustLimit: dustLimit, maxInputs: maxInputs, wallet: wallet, @@ -146,8 +142,7 @@ func (t *txInputSet) addToState(inp input.Input, constraints addConstraints) *tx s.inputTotal += value // Recalculate the tx fee. - weight := s.weightEstimate.weight() - fee := t.feePerKW.FeeForWeight(int64(weight)) + fee := s.weightEstimate.fee() // Calculate the new output value. s.outputValue = s.inputTotal - fee diff --git a/sweep/txgenerator.go b/sweep/txgenerator.go index 3b98239d..f023c317 100644 --- a/sweep/txgenerator.go +++ b/sweep/txgenerator.go @@ -134,9 +134,9 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte, currentBlockHeight uint32, feePerKw chainfee.SatPerKWeight, signer input.Signer) (*wire.MsgTx, error) { - inputs, txWeight := getWeightEstimate(inputs) + inputs, estimator := getWeightEstimate(inputs, feePerKw) - txFee := feePerKw.FeeForWeight(txWeight) + txFee := estimator.fee() // Sum up the total value contained in the inputs. var totalSum btcutil.Amount @@ -208,21 +208,29 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte, } log.Infof("Creating sweep transaction %v for %v inputs (%s) "+ - "using %v sat/kw, tx_fee=%v", sweepTx.TxHash(), len(inputs), - inputTypeSummary(inputs), int64(feePerKw), txFee) + "using %v sat/kw, tx_weight=%v, tx_fee=%v, parents_count=%v, "+ + "parents_fee=%v, parents_weight=%v", + sweepTx.TxHash(), len(inputs), + inputTypeSummary(inputs), int64(feePerKw), + estimator.weight(), txFee, + len(estimator.parents), estimator.parentsFee, + estimator.parentsWeight, + ) return sweepTx, nil } // getWeightEstimate returns a weight estimate for the given inputs. // Additionally, it returns counts for the number of csv and cltv inputs. -func getWeightEstimate(inputs []input.Input) ([]input.Input, int64) { +func getWeightEstimate(inputs []input.Input, feeRate chainfee.SatPerKWeight) ( + []input.Input, *weightEstimator) { + // We initialize a weight estimator so we can accurately asses the // amount of fees we need to pay for this sweep transaction. // // TODO(roasbeef): can be more intelligent about buffering outputs to // be more efficient on-chain. - weightEstimate := newWeightEstimator() + weightEstimate := newWeightEstimator(feeRate) // Our sweep transaction will pay to a single segwit p2wkh address, // ensure it contributes to our weight estimate. @@ -247,7 +255,7 @@ func getWeightEstimate(inputs []input.Input) ([]input.Input, int64) { sweepInputs = append(sweepInputs, inp) } - return sweepInputs, int64(weightEstimate.weight()) + return sweepInputs, weightEstimate } // inputSummary returns a string containing a human readable summary about the diff --git a/sweep/txgenerator_test.go b/sweep/txgenerator_test.go index 575ae65a..377ac24f 100644 --- a/sweep/txgenerator_test.go +++ b/sweep/txgenerator_test.go @@ -39,7 +39,8 @@ func TestWeightEstimate(t *testing.T) { )) } - _, weight := getWeightEstimate(inputs) + _, estimator := getWeightEstimate(inputs, 0) + weight := int64(estimator.weight()) if weight != expectedWeight { t.Fatalf("unexpected weight. expected %d but got %d.", expectedWeight, weight) diff --git a/sweep/walletsweep.go b/sweep/walletsweep.go index fb25ccf3..aa6d658d 100644 --- a/sweep/walletsweep.go +++ b/sweep/walletsweep.go @@ -255,7 +255,9 @@ func CraftSweepAllTx(feeRate chainfee.SatPerKWeight, blockHeight uint32, // Now that we've constructed the items required, we'll make an // input which can be passed to the sweeper for ultimate // sweeping. - input := input.MakeBaseInput(&output.OutPoint, witnessType, signDesc, 0) + input := input.MakeBaseInput( + &output.OutPoint, witnessType, signDesc, 0, nil, + ) inputsToSweep = append(inputsToSweep, &input) } diff --git a/sweep/weight_estimator.go b/sweep/weight_estimator.go index def29e7a..5a406815 100644 --- a/sweep/weight_estimator.go +++ b/sweep/weight_estimator.go @@ -1,33 +1,91 @@ package sweep import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) -// weightEstimator wraps a standard weight estimator instance. +// weightEstimator wraps a standard weight estimator instance and adds to that +// support for child-pays-for-parent. type weightEstimator struct { - estimator input.TxWeightEstimator + estimator input.TxWeightEstimator + feeRate chainfee.SatPerKWeight + parents map[chainhash.Hash]struct{} + parentsFee btcutil.Amount + parentsWeight int64 } // newWeightEstimator instantiates a new sweeper weight estimator. -func newWeightEstimator() *weightEstimator { - return &weightEstimator{} +func newWeightEstimator(feeRate chainfee.SatPerKWeight) *weightEstimator { + return &weightEstimator{ + feeRate: feeRate, + parents: make(map[chainhash.Hash]struct{}), + } } // clone returns a copy of this weight estimator. func (w *weightEstimator) clone() *weightEstimator { + parents := make(map[chainhash.Hash]struct{}, len(w.parents)) + for hash := range w.parents { + parents[hash] = struct{}{} + } + return &weightEstimator{ - estimator: w.estimator, + estimator: w.estimator, + feeRate: w.feeRate, + parents: parents, + parentsFee: w.parentsFee, + parentsWeight: w.parentsWeight, } } // add adds the weight of the given input to the weight estimate. func (w *weightEstimator) add(inp input.Input) error { + // If there is a parent tx, add the parent's fee and weight. + w.tryAddParent(inp) + wt := inp.WitnessType() return wt.AddWeightEstimation(&w.estimator) } +// tryAddParent examines the input and updates parent tx totals if required for +// cpfp. +func (w *weightEstimator) tryAddParent(inp input.Input) { + // Get unconfirmed parent info from the input. + unconfParent := inp.UnconfParent() + + // If there is no parent, there is nothing to add. + if unconfParent == nil { + return + } + + // If we've already accounted for the parent tx, don't do it + // again. This can happens when two outputs of the parent tx are + // included in the same sweep tx. + parentHash := inp.OutPoint().Hash + if _, ok := w.parents[parentHash]; ok { + return + } + + // Calculate parent fee rate. + parentFeeRate := chainfee.SatPerKWeight(unconfParent.Fee) * 1000 / + chainfee.SatPerKWeight(unconfParent.Weight) + + // Ignore parents that pay at least the fee rate of this transaction. + // Parent pays for child is not happening. + if parentFeeRate >= w.feeRate { + return + } + + // Include parent. + w.parents[parentHash] = struct{}{} + w.parentsFee += unconfParent.Fee + w.parentsWeight += unconfParent.Weight +} + // addP2WKHOutput updates the weight estimate to account for an additional // native P2WKH output. func (w *weightEstimator) addP2WKHOutput() { @@ -38,3 +96,25 @@ func (w *weightEstimator) addP2WKHOutput() { func (w *weightEstimator) weight() int { return w.estimator.Weight() } + +// fee returns the tx fee to use for the aggregated inputs and outputs, taking +// into account unconfirmed parent transactions (cpfp). +func (w *weightEstimator) fee() btcutil.Amount { + // Calculate fee and weight for just this tx. + childWeight := int64(w.estimator.Weight()) + + // Add combined weight of unconfirmed parent txes. + totalWeight := childWeight + w.parentsWeight + + // Subtract fee already paid by parents. + fee := w.feeRate.FeeForWeight(totalWeight) - w.parentsFee + + // Clamp the fee to what would be required if no parent txes were paid + // for. This is to make sure no rounding errors can get us into trouble. + childFee := w.feeRate.FeeForWeight(childWeight) + if childFee > fee { + fee = childFee + } + + return fee +} diff --git a/sweep/weight_estimator_test.go b/sweep/weight_estimator_test.go new file mode 100644 index 00000000..e4f1d489 --- /dev/null +++ b/sweep/weight_estimator_test.go @@ -0,0 +1,79 @@ +package sweep + +import ( + "testing" + + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/stretchr/testify/require" +) + +// TestWeightEstimator tests weight estimation for inputs with and without +// unconfirmed parents. +func TestWeightEstimator(t *testing.T) { + testFeeRate := chainfee.SatPerKWeight(20000) + + w := newWeightEstimator(testFeeRate) + + // Add an input without unconfirmed parent tx. + input1 := input.MakeBaseInput( + &wire.OutPoint{}, input.CommitmentAnchor, + &input.SignDescriptor{}, 0, nil, + ) + + require.NoError(t, w.add(&input1)) + + // The expectations is that this input is added. + const expectedWeight1 = 322 + require.Equal(t, expectedWeight1, w.weight()) + require.Equal(t, testFeeRate.FeeForWeight(expectedWeight1), w.fee()) + + // Define a parent transaction that pays a fee of 30000 sat/kw. + parentTxHighFee := &input.TxInfo{ + Weight: 100, + Fee: 3000, + } + + // Add an output of the parent tx above. + input2 := input.MakeBaseInput( + &wire.OutPoint{}, input.CommitmentAnchor, + &input.SignDescriptor{}, 0, + parentTxHighFee, + ) + + require.NoError(t, w.add(&input2)) + + // Pay for parent isn't possible because the parent pays a higher fee + // rate than the child. We expect no additional fee on the child. + const expectedWeight2 = expectedWeight1 + 280 + require.Equal(t, expectedWeight2, w.weight()) + require.Equal(t, testFeeRate.FeeForWeight(expectedWeight2), w.fee()) + + // Define a parent transaction that pays a fee of 10000 sat/kw. + parentTxLowFee := &input.TxInfo{ + Weight: 100, + Fee: 1000, + } + + // Add an output of the low-fee parent tx above. + input3 := input.MakeBaseInput( + &wire.OutPoint{}, input.CommitmentAnchor, + &input.SignDescriptor{}, 0, + parentTxLowFee, + ) + require.NoError(t, w.add(&input3)) + + // Expect the weight to increase because of the third input. + const expectedWeight3 = expectedWeight2 + 280 + require.Equal(t, expectedWeight3, w.weight()) + + // Expect the fee to cover the child and the parent transaction at 20 + // sat/kw after subtraction of the fee that was already paid by the + // parent. + expectedFee := testFeeRate.FeeForWeight( + expectedWeight3+parentTxLowFee.Weight, + ) - parentTxLowFee.Fee + + require.Equal(t, expectedFee, w.fee()) +}