diff --git a/client.go b/client.go index f116f6b38..2254cf895 100644 --- a/client.go +++ b/client.go @@ -62,8 +62,14 @@ var ( // probeTimeout is the maximum time until a probe is allowed to take. probeTimeout = 3 * time.Minute + // repushDelay is the delay of (re)adding a sweep to sweepbatcher after + // a block is mined. repushDelay = 1 * time.Second + // additionalDelay is the delay added on top of repushDelay inside the + // sweepbatcher to publish a sweep transaction. + additionalDelay = 1 * time.Second + // MinerFeeEstimationFailed is a magic number that is returned in a // quote call as the miner fee if the fee estimation in lnd's wallet // failed because of insufficient funds. @@ -185,13 +191,52 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore, "NewSweepFetcherFromSwapStore failed: %w", err) } + // There is circular dependency between executor and sweepbatcher, as + // executor stores sweepbatcher and sweepbatcher depends on + // executor.height() though loopOutSweepFeerateProvider. + var executor *executor + + // getHeight returns current height, according to executor. + getHeight := func() int32 { + if executor == nil { + // This must not happen, because executor is set in this + // function, before sweepbatcher.Run is called. + log.Errorf("getHeight called when executor is nil") + + return 0 + } + + return executor.height() + } + + loopOutSweepFeerateProvider := newLoopOutSweepFeerateProvider( + sweeper, loopDB, cfg.Lnd.ChainParams, getHeight, + ) + batcher := sweepbatcher.NewBatcher( cfg.Lnd.WalletKit, cfg.Lnd.ChainNotifier, cfg.Lnd.Signer, swapServerClient.MultiMuSig2SignSweep, verifySchnorrSig, cfg.Lnd.ChainParams, sweeperDb, sweepStore, + + // Disable 100 sats/kw fee bump every block and retarget feerate + // every block according to the current mempool condition. + sweepbatcher.WithCustomFeeRate( + loopOutSweepFeerateProvider.GetMinFeeRate, + ), + + // Upon new block arrival, republishing is triggered in both + // loopout.go code (waitForHtlcSpendConfirmedV2/ <-timerChan) + // and in sweepbatcher code (batch.Run/case <-timerChan). The + // former updates the fee rate which is used by the later by + // calling AddSweep. Make sure they are ordered, add additional + // delay time to sweepbatcher's handling. The delay used in + // loopout.go is repushDelay. + sweepbatcher.WithPublishDelay( + repushDelay+additionalDelay, + ), ) - executor := newExecutor(&executorConfig{ + executor = newExecutor(&executorConfig{ lnd: cfg.Lnd, store: loopDB, sweeper: sweeper, @@ -570,8 +615,11 @@ func (s *Client) getLoopOutSweepFee(ctx context.Context, confTarget int32) ( htlc = swap.QuoteHtlcP2WSH } + label := "loopout-quote" + return s.sweeper.GetSweepFee( ctx, htlc.AddSuccessToEstimator, p2wshAddress, confTarget, + label, ) } diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index 8412e0784..fbd842b1f 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -106,13 +106,13 @@ const ( // time we reach timeout. We set this to a high estimate so that we can // account for worst-case fees, (1250 * 4 / 1000) = 50 sat/byte. defaultLoopInSweepFee = chainfee.SatPerKWeight(1250) -) -var ( // defaultHtlcConfTarget is the default confirmation target we use for // loop in swap htlcs, set to the same default at the client. defaultHtlcConfTarget = loop.DefaultHtlcConfTarget +) +var ( // defaultBudget is the default autoloop budget we set. This budget will // only be used for automatically dispatched swaps if autoloop is // explicitly enabled, so we are happy to set a non-zero value here. The diff --git a/loopd/log.go b/loopd/log.go index a4f433f01..6ef5a74be 100644 --- a/loopd/log.go +++ b/loopd/log.go @@ -11,6 +11,7 @@ import ( "github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/notifications" + "github.com/lightninglabs/loop/sweep" "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/build" @@ -52,6 +53,9 @@ func SetupLoggers(root *build.RotatingLogWriter, intercept signal.Interceptor) { lnd.AddSubLogger( root, notifications.Subsystem, intercept, notifications.UseLogger, ) + lnd.AddSubLogger( + root, sweep.Subsystem, intercept, sweep.UseLogger, + ) } // genSubLogger creates a logger for a subsystem. We provide an instance of diff --git a/loopin.go b/loopin.go index b72d1795f..21e40dcba 100644 --- a/loopin.go +++ b/loopin.go @@ -1077,10 +1077,12 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context, } } + label := fmt.Sprintf("loopin-timeout-%x", s.hash[:6]) + // Calculate sweep tx fee. fee, err := s.sweeper.GetSweepFee( ctx, s.htlc.AddTimeoutToEstimator, s.timeoutAddr, - TimeoutTxConfTarget, + TimeoutTxConfTarget, label, ) if err != nil { return 0, err diff --git a/loopin_test.go b/loopin_test.go index 32f052100..5f403ba81 100644 --- a/loopin_test.go +++ b/loopin_test.go @@ -312,11 +312,13 @@ func handleHtlcExpiry(t *testing.T, ctx *loopInTestContext, inSwap *loopInSwap, // Expect timeout tx to be published. timeoutTx := <-ctx.lnd.TxPublishChannel + label := fmt.Sprintf("loopin-timeout-%x", inSwap.hash[:6]) + // We can just get our sweep fee as we would in the swap code because // our estimate is static. fee, err := inSwap.sweeper.GetSweepFee( context.Background(), inSwap.htlc.AddTimeoutToEstimator, - inSwap.timeoutAddr, TimeoutTxConfTarget, + inSwap.timeoutAddr, TimeoutTxConfTarget, label, ) require.NoError(t, err) cost.Onchain += fee diff --git a/loopout.go b/loopout.go index 6be118c15..c866ef60d 100644 --- a/loopout.go +++ b/loopout.go @@ -38,27 +38,33 @@ const ( // We'll try to sweep with MuSig2 at most 10 times. If that fails we'll // fail back to using standard scriptspend sweep. maxMusigSweepRetries = 10 -) -var ( // MinLoopOutPreimageRevealDelta configures the minimum number of // remaining blocks before htlc expiry required to reveal preimage. - MinLoopOutPreimageRevealDelta int32 = 20 + MinLoopOutPreimageRevealDelta = 20 // DefaultSweepConfTarget is the default confirmation target we'll use // when sweeping on-chain HTLCs. - DefaultSweepConfTarget int32 = 9 + DefaultSweepConfTarget = 9 // DefaultHtlcConfTarget is the default confirmation target we'll use // for on-chain htlcs published by the swap client for Loop In. - DefaultHtlcConfTarget int32 = 6 + DefaultHtlcConfTarget = 6 // DefaultSweepConfTargetDelta is the delta of blocks from a Loop Out - // swap's expiration height at which we begin to use the default sweep - // confirmation target. - // - // TODO(wilmer): tune? - DefaultSweepConfTargetDelta = DefaultSweepConfTarget * 2 + // swap's expiration height at which we begin to cap the sweep + // confirmation target with urgentSweepConfTarget and multiply feerate + // by factor urgentSweepConfTargetFactor. + DefaultSweepConfTargetDelta = 10 + + // urgentSweepConfTarget is the confirmation target we'll use when the + // loop-out swap is about to expire (<= DefaultSweepConfTargetDelta + // blocks to expire). + urgentSweepConfTarget = 3 + + // urgentSweepConfTargetFactor is the factor we apply to feerate of + // loop-out sweep if it is about to expire. + urgentSweepConfTargetFactor = 1.1 ) // loopOutSwap contains all the in-memory state related to a pending loop out @@ -1169,12 +1175,12 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmedV2(globalCtx context.Context, timerChan = s.timerFactory(repushDelay) case <-timerChan: - // sweepConfTarget will return false if the preimage is + // canSweep will return false if the preimage is // not revealed yet but the conf target is closer than // 20 blocks. In this case to be sure we won't attempt // to sweep at all and we won't reveal the preimage // either. - _, canSweep := s.sweepConfTarget() + canSweep := s.canSweep() if !canSweep { s.log.Infof("Aborting swap, timed " + "out on-chain") @@ -1375,9 +1381,9 @@ func validateLoopOutContract(lnd *lndclient.LndServices, request *OutRequest, return nil } -// sweepConfTarget returns the confirmation target for the htlc sweep or false -// if we're too late. -func (s *loopOutSwap) sweepConfTarget() (int32, bool) { +// canSweep will return false if the preimage is not revealed yet but the conf +// target is closer than 20 blocks (i.e. it is too late to reveal the preimage). +func (s *loopOutSwap) canSweep() bool { remainingBlocks := s.CltvExpiry - s.height blocksToLastReveal := remainingBlocks - MinLoopOutPreimageRevealDelta preimageRevealed := s.state == loopdb.StatePreimageRevealed @@ -1393,20 +1399,8 @@ func (s *loopOutSwap) sweepConfTarget() (int32, bool) { s.height) s.state = loopdb.StateFailTimeout - return 0, false - } - - // Calculate the transaction fee based on the confirmation target - // required to sweep the HTLC before the timeout. We'll use the - // confirmation target provided by the client unless we've come too - // close to the expiration height, in which case we'll use the default - // if it is better than what the client provided. - confTarget := s.SweepConfTarget - if remainingBlocks <= DefaultSweepConfTargetDelta && - confTarget > DefaultSweepConfTarget { - - confTarget = DefaultSweepConfTarget + return false } - return confTarget, true + return true } diff --git a/loopout_feerate.go b/loopout_feerate.go new file mode 100644 index 000000000..91f62fcfb --- /dev/null +++ b/loopout_feerate.go @@ -0,0 +1,198 @@ +package loop + +import ( + "context" + "fmt" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/swap" + "github.com/lightninglabs/loop/utils" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" +) + +// sweeper provides fee, fee rate and weight by confTarget. +type sweeper interface { + // GetSweepFeeDetails calculates the required tx fee to spend to + // destAddr. It takes a function that is expected to add the weight of + // the input to the weight estimator. It also takes a label used for + // logging. It returns also the fee rate and transaction weight. + GetSweepFeeDetails(ctx context.Context, + addInputEstimate func(*input.TxWeightEstimator) error, + destAddr btcutil.Address, sweepConfTarget int32, label string) ( + btcutil.Amount, chainfee.SatPerKWeight, lntypes.WeightUnit, + error) +} + +// loopOutFetcher provides the loop out swap with the given hash. +type loopOutFetcher interface { + // FetchLoopOutSwap returns the loop out swap with the given hash. + FetchLoopOutSwap(ctx context.Context, + hash lntypes.Hash) (*loopdb.LoopOut, error) +} + +// heightGetter returns current height known to the swap server. +type heightGetter func() int32 + +// loopOutSweepFeerateProvider provides sweepbatcher with the info about swap's +// current feerate for loop-out sweep. +type loopOutSweepFeerateProvider struct { + // sweeper provides fee, fee rate and weight by confTarget. + sweeper sweeper + + // loopOutFetcher loads LoopOut from DB by swap hash. + loopOutFetcher loopOutFetcher + + // chainParams are the chain parameters of the chain that is used by + // swaps. + chainParams *chaincfg.Params + + // getHeight returns current height known to the swap server. + getHeight heightGetter +} + +// newLoopOutSweepFeerateProvider builds and returns new instance of +// loopOutSweepFeerateProvider. +func newLoopOutSweepFeerateProvider(sweeper sweeper, + loopOutFetcher loopOutFetcher, chainParams *chaincfg.Params, + getHeight heightGetter) *loopOutSweepFeerateProvider { + + return &loopOutSweepFeerateProvider{ + sweeper: sweeper, + loopOutFetcher: loopOutFetcher, + chainParams: chainParams, + getHeight: getHeight, + } +} + +// GetMinFeeRate returns minimum required feerate for a sweep by swap hash. +func (p *loopOutSweepFeerateProvider) GetMinFeeRate(ctx context.Context, + swapHash lntypes.Hash) (chainfee.SatPerKWeight, error) { + + _, feeRate, err := p.GetConfTargetAndFeeRate(ctx, swapHash) + + return feeRate, err +} + +// GetConfTargetAndFeeRate returns conf target and minimum required feerate +// for a sweep by swap hash. +func (p *loopOutSweepFeerateProvider) GetConfTargetAndFeeRate( + ctx context.Context, swapHash lntypes.Hash) (int32, + chainfee.SatPerKWeight, error) { + + // Load the loop-out from DB. + loopOut, err := p.loopOutFetcher.FetchLoopOutSwap(ctx, swapHash) + if err != nil { + return 0, 0, fmt.Errorf("failed to load swap %x from DB: %w", + swapHash[:6], err) + } + + contract := loopOut.Contract + if contract == nil { + return 0, 0, fmt.Errorf("loop-out %x has nil Contract", + swapHash[:6]) + } + + // Determine if we can keyspend. + htlcVersion := utils.GetHtlcScriptVersion(contract.ProtocolVersion) + canKeyspend := htlcVersion >= swap.HtlcV3 + + // Find addInputToEstimator function. + var addInputToEstimator func(e *input.TxWeightEstimator) error + if canKeyspend { + // Assume the server is cooperative and we produce keyspend. + addInputToEstimator = func(e *input.TxWeightEstimator) error { + e.AddTaprootKeySpendInput(txscript.SigHashDefault) + + return nil + } + } else { + // Get the HTLC script for our swap. + htlc, err := utils.GetHtlc( + swapHash, &contract.SwapContract, p.chainParams, + ) + if err != nil { + return 0, 0, fmt.Errorf("failed to get HTLC: %w", err) + } + addInputToEstimator = htlc.AddSuccessToEstimator + } + + // Transaction weight might be important for feeRate, in case of high + // priority proportional fee, so we accurately assess the size of input. + // The size of output is almost the same for all types, so use P2TR. + var destAddr *btcutil.AddressTaproot + + // Get current height. + height := p.getHeight() + if height == 0 { + return 0, 0, fmt.Errorf("got zero best block height") + } + + // blocksUntilExpiry is the number of blocks until the htlc timeout path + // opens for the client to sweep. + blocksUntilExpiry := contract.CltvExpiry - height + + // Find confTarget. If the sweep has expired, use confTarget=1, because + // confTarget must be positive. + confTarget := blocksUntilExpiry + if confTarget <= 0 { + log.Infof("Swap %x has expired (blocksUntilExpiry=%d), using "+ + "confTarget=1 for it.", swapHash[:6], blocksUntilExpiry) + + confTarget = 1 + } + + feeFactor := float64(1.0) + + // If confTarget is less than or equal to DefaultSweepConfTargetDelta, + // cap it with urgentSweepConfTarget and apply fee factor. + if confTarget <= DefaultSweepConfTargetDelta { + // If confTarget is already <= urgentSweepConfTarget, don't + // increase it. + newConfTarget := int32(urgentSweepConfTarget) + if confTarget < newConfTarget { + newConfTarget = confTarget + } + + log.Infof("Swap %x is about to expire (blocksUntilExpiry=%d), "+ + "reducing its confTarget from %d to %d and multiplying"+ + " feerate by %v.", swapHash[:6], blocksUntilExpiry, + confTarget, newConfTarget, urgentSweepConfTargetFactor) + + confTarget = newConfTarget + feeFactor = urgentSweepConfTargetFactor + } + + // Construct the label. + label := fmt.Sprintf("loopout-sweep-%x", swapHash[:6]) + + // Estimate confTarget and feeRate. + _, feeRate, _, err := p.sweeper.GetSweepFeeDetails( + ctx, addInputToEstimator, destAddr, confTarget, label, + ) + if err != nil { + return 0, 0, fmt.Errorf("fee estimator failed, swapHash=%x, "+ + "confTarget=%d: %w", swapHash[:6], confTarget, err) + } + + // Multiply feerate by fee factor. + feeRate = chainfee.SatPerKWeight(float64(feeRate) * feeFactor) + + // Sanity check. Make sure fee rate is not too low. + const minFeeRate = chainfee.AbsoluteFeePerKwFloor + if feeRate < minFeeRate { + log.Infof("Got too low fee rate for swap %x: %v. Increasing "+ + "it to %v.", swapHash[:6], feeRate, minFeeRate) + + feeRate = minFeeRate + } + + log.Debugf("Estimated for swap %x: feeRate=%s, confTarget=%d.", + swapHash[:6], feeRate, confTarget) + + return confTarget, feeRate, nil +} diff --git a/loopout_feerate_test.go b/loopout_feerate_test.go new file mode 100644 index 000000000..0e9690539 --- /dev/null +++ b/loopout_feerate_test.go @@ -0,0 +1,278 @@ +package loop + +import ( + "context" + "fmt" + "testing" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/sweep" + "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/stretchr/testify/require" +) + +// testSweeper is implementation of sweeper.Sweeper for test. +type testSweeper struct { +} + +// GetSweepFeeDetails calculates the required tx fee to spend to destAddr. It +// takes a function that is expected to add the weight of the input to the +// weight estimator. It returns also the fee rate and transaction weight. +func (s testSweeper) GetSweepFeeDetails(ctx context.Context, + addInputEstimate func(*input.TxWeightEstimator) error, + destAddr btcutil.Address, sweepConfTarget int32, + label string) (btcutil.Amount, chainfee.SatPerKWeight, + lntypes.WeightUnit, error) { + + var feeRate chainfee.SatPerKWeight + switch { + case sweepConfTarget == 0: + return 0, 0, 0, fmt.Errorf("zero sweepConfTarget") + + case sweepConfTarget == 1: + feeRate = 30000 + + case sweepConfTarget == 2: + feeRate = 25000 + + case sweepConfTarget == 3: + feeRate = 20000 + + case sweepConfTarget < 10: + feeRate = 8000 + + case sweepConfTarget < 100: + feeRate = 5000 + + case sweepConfTarget < 1000: + feeRate = 2000 + + default: + feeRate = 250 + } + + // Calculate weight for this tx. + var weightEstimate input.TxWeightEstimator + + // Add output. + err := sweep.AddOutputEstimate(&weightEstimate, destAddr) + if err != nil { + return 0, 0, 0, fmt.Errorf("failed to add output weight "+ + "estimate: %w", err) + } + + // Add input. + err = addInputEstimate(&weightEstimate) + if err != nil { + return 0, 0, 0, fmt.Errorf("failed to add input weight "+ + "estimate: %w", err) + } + + // Find weight. + weight := weightEstimate.Weight() + + return feeRate.FeeForWeight(weight), feeRate, weight, nil +} + +// TestLoopOutSweepFeerateProvider tests that loopOutSweepFeerateProvider +// provides correct fee rate for loop-out swaps. +func TestLoopOutSweepFeerateProvider(t *testing.T) { + htlcKeys := func() loopdb.HtlcKeys { + var senderKey, receiverKey [33]byte + + // Generate keys. + _, senderPubKey := test.CreateKey(1) + copy(senderKey[:], senderPubKey.SerializeCompressed()) + _, receiverPubKey := test.CreateKey(2) + copy(receiverKey[:], receiverPubKey.SerializeCompressed()) + + return loopdb.HtlcKeys{ + SenderScriptKey: senderKey, + ReceiverScriptKey: receiverKey, + SenderInternalPubKey: senderKey, + ReceiverInternalPubKey: receiverKey, + } + }() + + var destAddr *btcutil.AddressTaproot + + swapInvoice := "lntb1230n1pjjszzgpp5j76f03wrkya4sm4gxv6az5nmz5aqsvmn4" + + "tpguu2sdvdyygedqjgqdq9xyerxcqzzsxqr23ssp5rwzmwtfjmsgranfk8sr" + + "4p4gcgmvyd42uug8pxteg2mkk23ndvkqs9qyyssq44ruk3ex59cmv4dm6k4v" + + "0kc6c0gcqjs0gkljfyd6c6uatqa2f67xlx3pcg5tnvcae5p3jju8ra77e87d" + + "vhhs0jrx53wnc0fq9rkrhmqqelyx7l" + + cases := []struct { + name string + cltvExpiry int32 + height int32 + amount btcutil.Amount + protocolVersion loopdb.ProtocolVersion + wantConfTarget int32 + wantFeeRate chainfee.SatPerKWeight + wantError string + }{ + { + name: "simple case", + cltvExpiry: 801_000, + height: 800_900, + amount: 1_000_000, + protocolVersion: loopdb.ProtocolVersionMuSig2, + wantConfTarget: 100, + wantFeeRate: 2000, + }, + { + name: "zero height", + cltvExpiry: 801_000, + height: 0, + amount: 1_000_000, + protocolVersion: loopdb.ProtocolVersionMuSig2, + wantError: "got zero best block height", + }, + { + name: "huge amount, no proportional fee", + cltvExpiry: 801_000, + height: 800_900, + amount: 100_000_000, + protocolVersion: loopdb.ProtocolVersionMuSig2, + wantConfTarget: 100, + wantFeeRate: 2000, + }, + { + name: "huge amount, no proportional fee, v2", + cltvExpiry: 801_000, + height: 800_900, + amount: 100_000_000, + protocolVersion: loopdb.ProtocolVersionLoopOutCancel, + wantConfTarget: 100, + wantFeeRate: 2000, + }, + { + name: "huge amount, no proportional fee, " + + "capped by urgent fee", + cltvExpiry: 801_000, + height: 800_900, + amount: 200_000_000, + protocolVersion: loopdb.ProtocolVersionMuSig2, + wantConfTarget: 100, + wantFeeRate: 2000, + }, + { + name: "11 blocks until expiry", + cltvExpiry: 801_000, + height: 800_989, + amount: 1_000_000, + protocolVersion: loopdb.ProtocolVersionMuSig2, + wantConfTarget: 11, + wantFeeRate: 5000, + }, + { + name: "10 blocks until expiry", + cltvExpiry: 801_000, + height: 800_990, + amount: 1_000_000, + protocolVersion: loopdb.ProtocolVersionMuSig2, + wantConfTarget: 3, + wantFeeRate: 22000, + }, + { + name: "9 blocks until expiry", + cltvExpiry: 801_000, + height: 800_991, + amount: 1_000_000, + protocolVersion: loopdb.ProtocolVersionMuSig2, + wantConfTarget: 3, + wantFeeRate: 22000, + }, + { + name: "3 blocks until expiry", + cltvExpiry: 801_000, + height: 800_997, + amount: 1_000_000, + protocolVersion: loopdb.ProtocolVersionMuSig2, + wantConfTarget: 3, + wantFeeRate: 22000, + }, + { + name: "2 blocks until expiry", + cltvExpiry: 801_000, + height: 800_998, + amount: 1_000_000, + protocolVersion: loopdb.ProtocolVersionMuSig2, + wantConfTarget: 2, + wantFeeRate: 27500, + }, + { + name: "1 blocks until expiry", + cltvExpiry: 801_000, + height: 800_999, + amount: 1_000_000, + protocolVersion: loopdb.ProtocolVersionMuSig2, + wantConfTarget: 1, + wantFeeRate: 33000, + }, + { + name: "expired", + cltvExpiry: 801_000, + height: 801_000, + amount: 1_000_000, + protocolVersion: loopdb.ProtocolVersionMuSig2, + wantConfTarget: 1, + wantFeeRate: 33000, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + store := loopdb.NewStoreMock(t) + + ctx := context.Background() + + swapHash := lntypes.Hash{1, 1, 1} + swap := &loopdb.LoopOutContract{ + SwapContract: loopdb.SwapContract{ + CltvExpiry: tc.cltvExpiry, + AmountRequested: tc.amount, + ProtocolVersion: tc.protocolVersion, + HtlcKeys: htlcKeys, + }, + + DestAddr: destAddr, + SwapInvoice: swapInvoice, + SweepConfTarget: 100, + } + + err := store.CreateLoopOut(ctx, swapHash, swap) + require.NoError(t, err) + store.AssertLoopOutStored() + + getHeight := func() int32 { + return tc.height + } + + p := newLoopOutSweepFeerateProvider( + testSweeper{}, store, + &chaincfg.RegressionNetParams, getHeight, + ) + + confTarget, feeRate, err := p.GetConfTargetAndFeeRate( + ctx, swapHash, + ) + if tc.wantError != "" { + require.ErrorContains(t, err, tc.wantError) + + return + } + + require.NoError(t, err) + require.Equal(t, tc.wantConfTarget, confTarget) + require.Equal(t, tc.wantFeeRate, feeRate) + }) + } +} diff --git a/sweep/log.go b/sweep/log.go new file mode 100644 index 000000000..3d0931a4c --- /dev/null +++ b/sweep/log.go @@ -0,0 +1,26 @@ +package sweep + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +// Subsystem defines the sub system name of this package. +const Subsystem = "SWP" + +// log is a logger that is initialized with no output filters. This means the +// package will not perform any logging by default until the caller requests +// it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// UseLogger uses a specified Logger to output package logging info. This +// should be used in preference to SetLogWriter if the caller is also using +// btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/sweep/sweeper.go b/sweep/sweeper.go index e6b3df462..edcce0256 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -177,15 +177,15 @@ func (s *Sweeper) CreateSweepTx( // GetSweepFee calculates the required tx fee to spend to P2WKH. It takes a // function that is expected to add the weight of the input to the weight -// estimator. +// estimator. It also takes a label used for logging. func (s *Sweeper) GetSweepFee(ctx context.Context, addInputEstimate func(*input.TxWeightEstimator) error, - destAddr btcutil.Address, sweepConfTarget int32) ( + destAddr btcutil.Address, sweepConfTarget int32, label string) ( btcutil.Amount, error) { // Use GetSweepFeeDetails to get the fee and other unused data. fee, _, _, err := s.GetSweepFeeDetails( - ctx, addInputEstimate, destAddr, sweepConfTarget, + ctx, addInputEstimate, destAddr, sweepConfTarget, label, ) return fee, err @@ -193,10 +193,11 @@ func (s *Sweeper) GetSweepFee(ctx context.Context, // GetSweepFeeDetails calculates the required tx fee to spend to P2WKH. It takes // a function that is expected to add the weight of the input to the weight -// estimator. It returns also the fee rate and transaction weight. +// estimator. It also takes a label used for logging. It returns also the fee +// rate and transaction weight. func (s *Sweeper) GetSweepFeeDetails(ctx context.Context, addInputEstimate func(*input.TxWeightEstimator) error, - destAddr btcutil.Address, sweepConfTarget int32) ( + destAddr btcutil.Address, sweepConfTarget int32, label string) ( btcutil.Amount, chainfee.SatPerKWeight, lntypes.WeightUnit, error) { // Get fee estimate from lnd. @@ -224,7 +225,14 @@ func (s *Sweeper) GetSweepFeeDetails(ctx context.Context, // Find weight. weight := weightEstimate.Weight() - return feeRate.FeeForWeight(weight), feeRate, weight, nil + // Find fee. + fee := feeRate.FeeForWeight(weight) + + log.Debugf("Estimations for a tx (label=%s): weight=%v, fee=%v, "+ + "feerate=%v, sweepConfTarget=%d.", label, weight, fee, feeRate, + sweepConfTarget) + + return fee, feeRate, weight, nil } // AddOutputEstimate adds output to weight estimator.