Browse Source

sweep: make sweeper aware of unconfirmed parent transactions.

Extend the fee estimator to take into account parent transactions with
their weights and fees.

Do not try to cpfp parent transactions that have a higher fee rate than
the sweep tx fee rate.
master
Joost Jager 4 years ago
parent
commit
681496b474
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7
  1. 5
      breacharbiter.go
  2. 1
      contractcourt/anchor_resolver.go
  3. 1
      contractcourt/channel_arbitrator.go
  4. 37
      input/input.go
  5. 25
      sweep/sweeper.go
  6. 87
      sweep/sweeper_test.go
  7. 1
      sweep/test_utils.go
  8. 9
      sweep/tx_input_set.go
  9. 22
      sweep/txgenerator.go
  10. 3
      sweep/txgenerator_test.go
  11. 4
      sweep/walletsweep.go
  12. 90
      sweep/weight_estimator.go
  13. 79
      sweep/weight_estimator_test.go

5
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)

1
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(

1
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

37
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

25
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

87
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)
}

1
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)
)

9
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

22
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

3
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)

4
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)
}

90
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
}

79
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())
}
Loading…
Cancel
Save