Skip to content

Commit 6a58e56

Browse files
committed
loopout: re-target sweep's feerate every block
Add type loopOutSweepFeerateProvider which uses new sweeper package (SweepFeeProvider), configures it with the constants of loopout.go (DefaultSweepConfTargetDelta, urgentSweepConfTarget, etc) and provides fee rate (and confTarget, used only in tests) for a swap. Fee rate is plugged into sweepbatcher using WithCustomFeeRate. Added new constants to loopout.go: highPrioSwapAmount, highPrioFeePPM, urgentSweepConfTarget, urgentSweepConfTargetFactor. The value of DefaultSweepConfTargetDelta to 10. Previous value was 18. The variable sets the number of blocks until expiry, when aggressive mode enables. 18 is too early. 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. If the swap is not of high priority (large amount) and isn't expiring soon (within DefaultSweepConfTargetDelta blocks), then confTarget is relative to cltvExpiry. Added tests for loopOutSweepFeerateProvider simulating various conditions.
1 parent ed8acab commit 6a58e56

File tree

4 files changed

+506
-6
lines changed

4 files changed

+506
-6
lines changed

client.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,45 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
185185
"NewSweepFetcherFromSwapStore failed: %w", err)
186186
}
187187

188+
// There is circular dependency between executor and sweepbatcher, as
189+
// executor stores sweepbatcher and sweepbatcher depends on
190+
// executor.height() though loopOutSweepFeerateProvider.
191+
var executor *executor
192+
193+
// getHeight returns current height, according to executor.
194+
getHeight := func() int32 {
195+
if executor == nil {
196+
// This must not happen, because executor is set in this
197+
// function, before sweepbatcher.Run is called.
198+
log.Errorf("getHeight called when executor is nil")
199+
200+
return 0
201+
}
202+
203+
return executor.height()
204+
}
205+
206+
loopOutSweepFeerateProvider, err := newLoopOutSweepFeerateProvider(
207+
sweeper, loopDB, cfg.Lnd.ChainParams, getHeight,
208+
)
209+
if err != nil {
210+
return nil, nil, fmt.Errorf("newLoopOutSweepFeerateProvider "+
211+
"failed: %w", err)
212+
}
213+
188214
batcher := sweepbatcher.NewBatcher(
189215
cfg.Lnd.WalletKit, cfg.Lnd.ChainNotifier, cfg.Lnd.Signer,
190216
swapServerClient.MultiMuSig2SignSweep, verifySchnorrSig,
191217
cfg.Lnd.ChainParams, sweeperDb, sweepStore,
218+
219+
// Disable 100 sats/kw fee bump every block and retarget feerate
220+
// every block according to the current mempool condition.
221+
sweepbatcher.WithCustomFeeRate(
222+
loopOutSweepFeerateProvider.GetMinFeeRate,
223+
),
192224
)
193225

194-
executor := newExecutor(&executorConfig{
226+
executor = newExecutor(&executorConfig{
195227
lnd: cfg.Lnd,
196228
store: loopDB,
197229
sweeper: sweeper,

loopout.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,32 @@ 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 sweep confirmation
56+
// target with urgentSweepConfTarget and multiply feerate by factor
57+
// 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 fee-rate of
66+
// loop-out sweep if it is about to expire.
67+
urgentSweepConfTargetFactor = 1.1
68+
69+
// highPrioSwapAmount is a threshold at which a swap is prioritized to
70+
// be swept with a fee that is related to its size. The fee amount is
71+
// determined by highPrioFeePPM. Current values correspond to 1 BTC
72+
// and 0.05% proportional fee.
73+
highPrioSwapAmount = 100_000_000
74+
75+
// highPrioFeePPM expresses the fraction of the swap amount that is paid
76+
// towards sweeping fees. This fee is only supposed to be paid for swaps
77+
// greater or equal the size of highPrioSwapAmount to expedite the
78+
// availability of on-chain funds to generate revenue on loop-out
79+
// operations.
80+
highPrioFeePPM = 50
6081
)
6182

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

loopout_feerate.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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+
sweeperpkg "github.com/lightninglabs/loop/sweeper"
13+
"github.com/lightninglabs/loop/utils"
14+
"github.com/lightningnetwork/lnd/input"
15+
"github.com/lightningnetwork/lnd/lntypes"
16+
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
17+
)
18+
19+
// sweepFeeGetter calculates sweep's confTarget and fee in sats.
20+
type sweepFeeGetter interface {
21+
// SweepFeeRate calculates sweep's confTarget and fee in sats.
22+
// It takes high priority case into account.
23+
SweepFeeRate(ctx context.Context, amt btcutil.Amount,
24+
addInputToEstimator func(e *input.TxWeightEstimator) error,
25+
destAddr btcutil.Address, cltvExpiry, height int32) (int32,
26+
btcutil.Amount, chainfee.SatPerKWeight, lntypes.WeightUnit,
27+
error)
28+
}
29+
30+
// loopOutFetcher provides the loop out swap with the given hash.
31+
type loopOutFetcher interface {
32+
// FetchLoopOutSwap returns the loop out swap with the given hash.
33+
FetchLoopOutSwap(ctx context.Context,
34+
hash lntypes.Hash) (*loopdb.LoopOut, error)
35+
}
36+
37+
// heightGetter returns current height known to the swap server.
38+
type heightGetter func() int32
39+
40+
// loopOutSweepFeerateProvider provides sweepbatcher with the info about swap's
41+
// current feerate for loop-out sweep.
42+
type loopOutSweepFeerateProvider struct {
43+
// sweepFeeGetter incorporates the logic of determining swap feerate.
44+
sweepFeeGetter sweepFeeGetter
45+
46+
// loopOutFetcher loads LoopOut from DB by swap hash.
47+
loopOutFetcher loopOutFetcher
48+
49+
// chainParams are the chain parameters of the chain that is used by
50+
// swaps.
51+
chainParams *chaincfg.Params
52+
53+
// getHeight returns current height known to the swap server.
54+
getHeight heightGetter
55+
}
56+
57+
// newLoopOutSweepFeerateProvider builds and returns new instance of
58+
// loopOutSweepFeerateProvider.
59+
func newLoopOutSweepFeerateProvider(sweeper sweeperpkg.Sweeper,
60+
loopOutFetcher loopOutFetcher, chainParams *chaincfg.Params,
61+
getHeight heightGetter) (*loopOutSweepFeerateProvider, error) {
62+
63+
// Initialize sweep fee provider for loop-out's.
64+
sweepFeeProvider := &sweeperpkg.SweepFeeProvider{
65+
Sweeper: sweeper,
66+
67+
DefaultSweepConfTargetDelta: DefaultSweepConfTargetDelta,
68+
UrgentSweepConfTarget: urgentSweepConfTarget,
69+
DefaultSweepConfTargetFactor: 1.0,
70+
UrgentSweepConfTargetFactor: urgentSweepConfTargetFactor,
71+
HighPrioSwapAmount: highPrioSwapAmount,
72+
HighPrioFeePPM: highPrioFeePPM,
73+
}
74+
75+
if err := sweepFeeProvider.Validate(); err != nil {
76+
return nil, fmt.Errorf("sweep fee provider failed: %w", err)
77+
}
78+
79+
return &loopOutSweepFeerateProvider{
80+
sweepFeeGetter: sweepFeeProvider,
81+
loopOutFetcher: loopOutFetcher,
82+
chainParams: chainParams,
83+
getHeight: getHeight,
84+
}, nil
85+
}
86+
87+
// GetMinFeeRate returns minimum required feerate for a sweep by swap hash.
88+
func (p *loopOutSweepFeerateProvider) GetMinFeeRate(ctx context.Context,
89+
swapHash lntypes.Hash) (chainfee.SatPerKWeight, error) {
90+
91+
_, feeRate, err := p.GetConfTargetAndFeeRate(ctx, swapHash)
92+
93+
return feeRate, err
94+
}
95+
96+
// GetConfTargetAndFeeRate returns conf target and minimum required feerate
97+
// for a sweep by swap hash.
98+
func (p *loopOutSweepFeerateProvider) GetConfTargetAndFeeRate(
99+
ctx context.Context, swapHash lntypes.Hash) (int32,
100+
chainfee.SatPerKWeight, error) {
101+
102+
// Load the loop-out from DB.
103+
loopOut, err := p.loopOutFetcher.FetchLoopOutSwap(ctx, swapHash)
104+
if err != nil {
105+
return 0, 0, fmt.Errorf("failed to load swap %x from DB: %w",
106+
swapHash[:6], err)
107+
}
108+
109+
contract := loopOut.Contract
110+
if contract == nil {
111+
return 0, 0, fmt.Errorf("loop-out %x has nil Contract",
112+
swapHash[:6])
113+
}
114+
115+
// Determine if we can keyspend.
116+
htlcVersion := utils.GetHtlcScriptVersion(contract.ProtocolVersion)
117+
canKeyspend := htlcVersion >= swap.HtlcV3
118+
119+
// Find addInputToEstimator function.
120+
var addInputToEstimator func(e *input.TxWeightEstimator) error
121+
if canKeyspend {
122+
// Assume the server is cooperative and we produce keyspend.
123+
addInputToEstimator = func(e *input.TxWeightEstimator) error {
124+
e.AddTaprootKeySpendInput(txscript.SigHashDefault)
125+
return nil
126+
}
127+
} else {
128+
// Get the HTLC script for our swap.
129+
htlc, err := utils.GetHtlc(
130+
swapHash, &contract.SwapContract, p.chainParams,
131+
)
132+
if err != nil {
133+
return 0, 0, fmt.Errorf("failed to get HTLC: %w", err)
134+
}
135+
addInputToEstimator = htlc.AddSuccessToEstimator
136+
}
137+
138+
// Transaction weight might be important for feeRate, in case of high
139+
// priority proportional fee, so we accurately assess the size of input.
140+
// The size of output is almost the same for all types, so use P2TR.
141+
var destAddr *btcutil.AddressTaproot
142+
143+
// Get current height.
144+
height := p.getHeight()
145+
if height == 0 {
146+
return 0, 0, fmt.Errorf("got zero best block height")
147+
}
148+
149+
// Estimate confTarget and feeRate.
150+
confTarget, _, feeRate, _, err := p.sweepFeeGetter.SweepFeeRate(
151+
ctx, contract.AmountRequested, addInputToEstimator, destAddr,
152+
contract.CltvExpiry, height,
153+
)
154+
if err != nil {
155+
return 0, 0, fmt.Errorf("failed to determine "+
156+
"confTarget and feeRate for swap hash %x: %w",
157+
swapHash[:6], err)
158+
}
159+
160+
log.Debugf("Estimated for swap %x: feeRate=%s, confTarget=%d.",
161+
swapHash[:6], feeRate, confTarget)
162+
163+
return confTarget, feeRate, nil
164+
}

0 commit comments

Comments
 (0)