mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-17 13:06:51 +02:00
Merge pull request #838 from starius/loopout-retarget-feerate-ever-block
loopout: re-target sweep's feerate every block
This commit is contained in:
commit
0fe952a5c7
10 changed files with 600 additions and 40 deletions
50
client.go
50
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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
52
loopout.go
52
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
|
||||
return 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 confTarget, true
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
198
loopout_feerate.go
Normal file
198
loopout_feerate.go
Normal file
|
|
@ -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
|
||||
}
|
||||
278
loopout_feerate_test.go
Normal file
278
loopout_feerate_test.go
Normal file
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
26
sweep/log.go
Normal file
26
sweep/log.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue