Skip to content

Commit 2f22f96

Browse files
committed
loopout: re-target sweep's feerate every block
Add type loopOutSweepFeerateProvider which determines confTarget based on distance to swap expiration, then determines feerate and fee using. Fee rate is plugged into sweepbatcher using WithCustomFeeRate. Option WithPublishDelay is used to make sure fee-rate is updated by loopout.go before the value is used by sweepbatcher. When determining confTarget, there are few adjustments over raw distance to cltv_expiry: - make sure confTarget is positive (if the swap has expired, raw distance is negative) - If confTarget is less than or equal to DefaultSweepConfTargetDelta (10), cap it with urgentSweepConfTarget and apply fee factor (1.1x). Also, if feerate is less than floor (1 sat/vbyte), then the floor is used. DefaultSweepConfTargetDelta was decreased from 18 to 10. Every block 100 sats/kw fee bump is disabled. Sweepbatcher re-targets feerate every block according to current mempool conditions and the number of blocks until expiry. Added tests for loopOutSweepFeerateProvider simulating various conditions.
1 parent 697e8d6 commit 2f22f96

File tree

4 files changed

+535
-6
lines changed

4 files changed

+535
-6
lines changed

client.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,14 @@ var (
6262
// probeTimeout is the maximum time until a probe is allowed to take.
6363
probeTimeout = 3 * time.Minute
6464

65+
// repushDelay is the delay of (re)adding a sweep to sweepbatcher after
66+
// a block is mined.
6567
repushDelay = 1 * time.Second
6668

69+
// additionalDelay is the delay added on top of repushDelay inside the
70+
// sweepbatcher to publish a sweep transaction.
71+
additionalDelay = 1 * time.Second
72+
6773
// MinerFeeEstimationFailed is a magic number that is returned in a
6874
// quote call as the miner fee if the fee estimation in lnd's wallet
6975
// failed because of insufficient funds.
@@ -185,13 +191,52 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
185191
"NewSweepFetcherFromSwapStore failed: %w", err)
186192
}
187193

194+
// There is circular dependency between executor and sweepbatcher, as
195+
// executor stores sweepbatcher and sweepbatcher depends on
196+
// executor.height() though loopOutSweepFeerateProvider.
197+
var executor *executor
198+
199+
// getHeight returns current height, according to executor.
200+
getHeight := func() int32 {
201+
if executor == nil {
202+
// This must not happen, because executor is set in this
203+
// function, before sweepbatcher.Run is called.
204+
log.Errorf("getHeight called when executor is nil")
205+
206+
return 0
207+
}
208+
209+
return executor.height()
210+
}
211+
212+
loopOutSweepFeerateProvider := newLoopOutSweepFeerateProvider(
213+
sweeper, loopDB, cfg.Lnd.ChainParams, getHeight,
214+
)
215+
188216
batcher := sweepbatcher.NewBatcher(
189217
cfg.Lnd.WalletKit, cfg.Lnd.ChainNotifier, cfg.Lnd.Signer,
190218
swapServerClient.MultiMuSig2SignSweep, verifySchnorrSig,
191219
cfg.Lnd.ChainParams, sweeperDb, sweepStore,
220+
221+
// Disable 100 sats/kw fee bump every block and retarget feerate
222+
// every block according to the current mempool condition.
223+
sweepbatcher.WithCustomFeeRate(
224+
loopOutSweepFeerateProvider.GetMinFeeRate,
225+
),
226+
227+
// Upon new block arrival, republishing is triggered in both
228+
// loopout.go code (waitForHtlcSpendConfirmedV2/ <-timerChan)
229+
// and in sweepbatcher code (batch.Run/case <-timerChan). The
230+
// former updates the fee rate which is used by the later by
231+
// calling AddSweep. Make sure they are ordered, add additional
232+
// delay time to sweepbatcher's handling. The delay used in
233+
// loopout.go is repushDelay.
234+
sweepbatcher.WithPublishDelay(
235+
repushDelay+additionalDelay,
236+
),
192237
)
193238

194-
executor := newExecutor(&executorConfig{
239+
executor = newExecutor(&executorConfig{
195240
lnd: cfg.Lnd,
196241
store: loopDB,
197242
sweeper: sweeper,

loopout.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,19 @@ const (
5252
DefaultHtlcConfTarget = 6
5353

5454
// DefaultSweepConfTargetDelta is the delta of blocks from a Loop Out
55-
// swap's expiration height at which we begin to use the default sweep
56-
// confirmation target.
57-
//
58-
// TODO(wilmer): tune?
59-
DefaultSweepConfTargetDelta = DefaultSweepConfTarget * 2
55+
// swap's expiration height at which we begin to cap the sweep
56+
// confirmation target with urgentSweepConfTarget and multiply feerate
57+
// by factor urgentSweepConfTargetFactor.
58+
DefaultSweepConfTargetDelta = 10
59+
60+
// urgentSweepConfTarget is the confirmation target we'll use when the
61+
// loop-out swap is about to expire (<= DefaultSweepConfTargetDelta
62+
// blocks to expire).
63+
urgentSweepConfTarget = 3
64+
65+
// urgentSweepConfTargetFactor is the factor we apply to feerate of
66+
// loop-out sweep if it is about to expire.
67+
urgentSweepConfTargetFactor = 1.1
6068
)
6169

6270
// loopOutSwap contains all the in-memory state related to a pending loop out

loopout_feerate.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package loop
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/btcsuite/btcd/btcutil"
8+
"github.com/btcsuite/btcd/chaincfg"
9+
"github.com/btcsuite/btcd/txscript"
10+
"github.com/lightninglabs/loop/loopdb"
11+
"github.com/lightninglabs/loop/swap"
12+
"github.com/lightninglabs/loop/utils"
13+
"github.com/lightningnetwork/lnd/input"
14+
"github.com/lightningnetwork/lnd/lntypes"
15+
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
16+
)
17+
18+
// sweeper provides fee, fee rate and weight by confTarget.
19+
type sweeper interface {
20+
// GetSweepFeeDetails calculates the required tx fee to spend to
21+
// destAddr. It takes a function that is expected to add the weight of
22+
// the input to the weight estimator. It also takes a label used for
23+
// logging. It returns also the fee rate and transaction weight.
24+
GetSweepFeeDetails(ctx context.Context,
25+
addInputEstimate func(*input.TxWeightEstimator) error,
26+
destAddr btcutil.Address, sweepConfTarget int32, label string) (
27+
btcutil.Amount, chainfee.SatPerKWeight, lntypes.WeightUnit,
28+
error)
29+
}
30+
31+
// loopOutFetcher provides the loop out swap with the given hash.
32+
type loopOutFetcher interface {
33+
// FetchLoopOutSwap returns the loop out swap with the given hash.
34+
FetchLoopOutSwap(ctx context.Context,
35+
hash lntypes.Hash) (*loopdb.LoopOut, error)
36+
}
37+
38+
// heightGetter returns current height known to the swap server.
39+
type heightGetter func() int32
40+
41+
// loopOutSweepFeerateProvider provides sweepbatcher with the info about swap's
42+
// current feerate for loop-out sweep.
43+
type loopOutSweepFeerateProvider struct {
44+
// sweeper provides fee, fee rate and weight by confTarget.
45+
sweeper sweeper
46+
47+
// loopOutFetcher loads LoopOut from DB by swap hash.
48+
loopOutFetcher loopOutFetcher
49+
50+
// chainParams are the chain parameters of the chain that is used by
51+
// swaps.
52+
chainParams *chaincfg.Params
53+
54+
// getHeight returns current height known to the swap server.
55+
getHeight heightGetter
56+
}
57+
58+
// newLoopOutSweepFeerateProvider builds and returns new instance of
59+
// loopOutSweepFeerateProvider.
60+
func newLoopOutSweepFeerateProvider(sweeper sweeper,
61+
loopOutFetcher loopOutFetcher, chainParams *chaincfg.Params,
62+
getHeight heightGetter) *loopOutSweepFeerateProvider {
63+
64+
return &loopOutSweepFeerateProvider{
65+
sweeper: sweeper,
66+
loopOutFetcher: loopOutFetcher,
67+
chainParams: chainParams,
68+
getHeight: getHeight,
69+
}
70+
}
71+
72+
// GetMinFeeRate returns minimum required feerate for a sweep by swap hash.
73+
func (p *loopOutSweepFeerateProvider) GetMinFeeRate(ctx context.Context,
74+
swapHash lntypes.Hash) (chainfee.SatPerKWeight, error) {
75+
76+
_, feeRate, err := p.GetConfTargetAndFeeRate(ctx, swapHash)
77+
78+
return feeRate, err
79+
}
80+
81+
// GetConfTargetAndFeeRate returns conf target and minimum required feerate
82+
// for a sweep by swap hash.
83+
func (p *loopOutSweepFeerateProvider) GetConfTargetAndFeeRate(
84+
ctx context.Context, swapHash lntypes.Hash) (int32,
85+
chainfee.SatPerKWeight, error) {
86+
87+
// Load the loop-out from DB.
88+
loopOut, err := p.loopOutFetcher.FetchLoopOutSwap(ctx, swapHash)
89+
if err != nil {
90+
return 0, 0, fmt.Errorf("failed to load swap %x from DB: %w",
91+
swapHash[:6], err)
92+
}
93+
94+
contract := loopOut.Contract
95+
if contract == nil {
96+
return 0, 0, fmt.Errorf("loop-out %x has nil Contract",
97+
swapHash[:6])
98+
}
99+
100+
// Determine if we can keyspend.
101+
htlcVersion := utils.GetHtlcScriptVersion(contract.ProtocolVersion)
102+
canKeyspend := htlcVersion >= swap.HtlcV3
103+
104+
// Find addInputToEstimator function.
105+
var addInputToEstimator func(e *input.TxWeightEstimator) error
106+
if canKeyspend {
107+
// Assume the server is cooperative and we produce keyspend.
108+
addInputToEstimator = func(e *input.TxWeightEstimator) error {
109+
e.AddTaprootKeySpendInput(txscript.SigHashDefault)
110+
111+
return nil
112+
}
113+
} else {
114+
// Get the HTLC script for our swap.
115+
htlc, err := utils.GetHtlc(
116+
swapHash, &contract.SwapContract, p.chainParams,
117+
)
118+
if err != nil {
119+
return 0, 0, fmt.Errorf("failed to get HTLC: %w", err)
120+
}
121+
addInputToEstimator = htlc.AddSuccessToEstimator
122+
}
123+
124+
// Transaction weight might be important for feeRate, in case of high
125+
// priority proportional fee, so we accurately assess the size of input.
126+
// The size of output is almost the same for all types, so use P2TR.
127+
var destAddr *btcutil.AddressTaproot
128+
129+
// Get current height.
130+
height := p.getHeight()
131+
if height == 0 {
132+
return 0, 0, fmt.Errorf("got zero best block height")
133+
}
134+
135+
// blocksUntilExpiry is the number of blocks until the htlc timeout path
136+
// opens for the client to sweep.
137+
blocksUntilExpiry := contract.CltvExpiry - height
138+
139+
// Find confTarget. If the sweep has expired, use confTarget=1, because
140+
// confTarget must be positive.
141+
confTarget := blocksUntilExpiry
142+
if confTarget <= 0 {
143+
log.Infof("Swap %x has expired (blocksUntilExpiry=%d), using "+
144+
"confTarget=1 for it.", swapHash[:6], blocksUntilExpiry)
145+
146+
confTarget = 1
147+
}
148+
149+
feeFactor := float64(1.0)
150+
151+
// If confTarget is less than or equal to DefaultSweepConfTargetDelta,
152+
// cap it with urgentSweepConfTarget and apply fee factor.
153+
if confTarget <= DefaultSweepConfTargetDelta {
154+
// If confTarget is already <= urgentSweepConfTarget, don't
155+
// increase it.
156+
newConfTarget := int32(urgentSweepConfTarget)
157+
if confTarget < newConfTarget {
158+
newConfTarget = confTarget
159+
}
160+
161+
log.Infof("Swap %x is about to expire (blocksUntilExpiry=%d), "+
162+
"reducing its confTarget from %d to %d and multiplying"+
163+
" feerate by %v.", swapHash[:6], blocksUntilExpiry,
164+
confTarget, newConfTarget, urgentSweepConfTargetFactor)
165+
166+
confTarget = newConfTarget
167+
feeFactor = urgentSweepConfTargetFactor
168+
}
169+
170+
// Construct the label.
171+
label := fmt.Sprintf("loopout-sweep-%x", swapHash[:6])
172+
173+
// Estimate confTarget and feeRate.
174+
_, feeRate, _, err := p.sweeper.GetSweepFeeDetails(
175+
ctx, addInputToEstimator, destAddr, confTarget, label,
176+
)
177+
if err != nil {
178+
return 0, 0, fmt.Errorf("fee estimator failed, swapHash=%x, "+
179+
"confTarget=%d: %w", swapHash[:6], confTarget, err)
180+
}
181+
182+
// Multiply feerate by fee factor.
183+
feeRate = chainfee.SatPerKWeight(float64(feeRate) * feeFactor)
184+
185+
// Sanity check. Make sure fee rate is not too low.
186+
const minFeeRate = chainfee.AbsoluteFeePerKwFloor
187+
if feeRate < minFeeRate {
188+
log.Infof("Got too low fee rate for swap %x: %v. Increasing "+
189+
"it to %v.", swapHash[:6], feeRate, minFeeRate)
190+
191+
feeRate = minFeeRate
192+
}
193+
194+
log.Debugf("Estimated for swap %x: feeRate=%s, confTarget=%d.",
195+
swapHash[:6], feeRate, confTarget)
196+
197+
return confTarget, feeRate, nil
198+
}

0 commit comments

Comments
 (0)