package loop import ( "context" "fmt" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "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, _ wire.OutPoint) (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=2, because // fee estimator fails if confTarget < 2. confTarget := blocksUntilExpiry if confTarget < 2 { log.Infof("Swap %x has expired (blocksUntilExpiry=%d), using "+ "confTarget=2 for it.", swapHash[:6], blocksUntilExpiry) confTarget = 2 } 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 := min(confTarget, int32(urgentSweepConfTarget)) 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 }