Skip to content

Commit ed8acab

Browse files
committed
add package sweeper
The package contains logic of determining sweep's conf target and fee rate.
1 parent 3eec626 commit ed8acab

File tree

3 files changed

+674
-0
lines changed

3 files changed

+674
-0
lines changed

sweeper/log.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package sweeper
2+
3+
import (
4+
"github.com/btcsuite/btclog"
5+
"github.com/lightningnetwork/lnd/build"
6+
)
7+
8+
// Subsystem defines the sub system name of this package.
9+
const Subsystem = "SWPR"
10+
11+
// log is a logger that is initialized with no output filters. This means the
12+
// package will not perform any logging by default until the caller requests
13+
// it.
14+
var log btclog.Logger
15+
16+
// The default amount of logging is none.
17+
func init() {
18+
UseLogger(build.NewSubLogger(Subsystem, nil))
19+
}
20+
21+
// UseLogger uses a specified Logger to output package logging info. This
22+
// should be used in preference to SetLogWriter if the caller is also using
23+
// btclog.
24+
func UseLogger(logger btclog.Logger) {
25+
log = logger
26+
}

sweeper/sweep_fee_provider.go

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package sweeper
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"github.com/btcsuite/btcd/btcutil"
8+
"github.com/lightningnetwork/lnd/input"
9+
"github.com/lightningnetwork/lnd/lntypes"
10+
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
11+
)
12+
13+
// Sweeper provides fee, fee rate and weight by confTarget.
14+
type Sweeper interface {
15+
// GetSweepFeeDetails calculates the required tx fee to spend to
16+
// destAddr. It takes a function that is expected to add the weight of
17+
// the input to the weight estimator. It returns also the fee rate and
18+
// transaction weight.
19+
GetSweepFeeDetails(ctx context.Context,
20+
addInputEstimate func(*input.TxWeightEstimator) error,
21+
destAddr btcutil.Address, sweepConfTarget int32) (
22+
btcutil.Amount, chainfee.SatPerKWeight, lntypes.WeightUnit,
23+
error)
24+
}
25+
26+
// SweepFeeProvider provides fee rate and fee for a loop sweep transaction.
27+
type SweepFeeProvider struct {
28+
// Sweeper returns fee rate by confTarget.
29+
Sweeper Sweeper
30+
31+
// DefaultSweepConfTargetDelta defines a range of remaining blocks in
32+
// which the block confirmation target in order to get a sweep
33+
// transaction confirmed before the on-chain htlc expires.
34+
DefaultSweepConfTargetDelta int32
35+
36+
// UrgentSweepConfTarget is the block confirmation target that is used
37+
// for fee calculation of the on-chain htlc sweep if we are close to
38+
// expiry.
39+
UrgentSweepConfTarget int32
40+
41+
// DefaultSweepConfTargetFactor is the default factor by which the fee
42+
// calculation increases the calculated fee.
43+
DefaultSweepConfTargetFactor float32
44+
45+
// UrgentSweepConfTargetFactor is the factor by which the fee
46+
// calculation increases the fee which resulted from the
47+
// UrgentSweepConfTarget confirmation target. This aims to ensure that
48+
// the sweep transaction will be confirmed asap in times where there is
49+
// elevated block space demand in the top fee band.
50+
UrgentSweepConfTargetFactor float32
51+
52+
// HighPrioSwapAmount is a threshold at which a swap is prioritized to
53+
// be swept with a fee that is related to its size. The fee amount is
54+
// determined by HighPrioFeePPM.
55+
HighPrioSwapAmount btcutil.Amount
56+
57+
// HighPrioFeePPM expresses the fraction of the swap amount that is paid
58+
// towards sweeping fees. This fee is only supposed to be paid for swaps
59+
// greater or equal the size of HighPrioSwapAmount to expedite the
60+
// availability of on-chain funds to generate revenue on loop-out
61+
// operations.
62+
// Example: Given HighPrioSwapAmount is 1.2 BTC, swap amount is 1.3 BTC
63+
// and HighPrioFeePPM is 100ppm, then we allocate 13k for the sweep fee,
64+
// but at most UrgentSweepConfTarget fees.
65+
HighPrioFeePPM int32
66+
}
67+
68+
// getSweepConfTarget calculates a dynamic block confirmation target for the
69+
// loop-in sweep transaction.
70+
func (p *SweepFeeProvider) getSweepConfTarget(cltvExpiry, height int32) (int32,
71+
float32) {
72+
73+
// blocksUntilExpiry is the number of blocks until the htlc timeout path
74+
// opens for the client to sweep.
75+
blocksUntilExpiry := cltvExpiry - height
76+
77+
// As we are approaching the expiry of the on-chain htlc we reduce the
78+
// block confirmation target to the number of blocks until expiry. If we
79+
// are within the DefaultSweepConfTargetDelta we return
80+
// UrgentSweepConfTarget and an adjusted fee factor.
81+
switch {
82+
case blocksUntilExpiry > p.DefaultSweepConfTargetDelta:
83+
return blocksUntilExpiry, p.DefaultSweepConfTargetFactor
84+
85+
// If we still have more than UrgentSweepConfTarget blocks to go,
86+
// use UrgentSweepConfTarget.
87+
case blocksUntilExpiry > p.UrgentSweepConfTarget:
88+
return p.UrgentSweepConfTarget, p.UrgentSweepConfTargetFactor
89+
90+
// If we are below UrgentSweepConfTarget, but still have more than 1
91+
// block to go, use the number of remaining blocks as target.
92+
case blocksUntilExpiry >= 1:
93+
return blocksUntilExpiry, p.UrgentSweepConfTargetFactor
94+
95+
// The swap has expired. Use confTarget=1. We should not be here.
96+
default:
97+
return 1, p.UrgentSweepConfTargetFactor
98+
}
99+
}
100+
101+
// getHighPriorityFee returns a swap amount adjusted sweep fee that does not
102+
// exceed the fee we assign for swaps close to expiry.
103+
func (p *SweepFeeProvider) getHighPriorityFee(ctx context.Context,
104+
addInputToEstimator func(e *input.TxWeightEstimator) error,
105+
destAddr btcutil.Address, amt btcutil.Amount) (btcutil.Amount,
106+
chainfee.SatPerKWeight, lntypes.WeightUnit, error) {
107+
108+
log.Infof("High priority swap amount: %v", amt)
109+
110+
urgentConfTargetFee, urgentConfTargetFeeRate, weight, err :=
111+
p.Sweeper.GetSweepFeeDetails(
112+
ctx, addInputToEstimator, destAddr,
113+
p.UrgentSweepConfTarget,
114+
)
115+
if err != nil {
116+
log.Warnf("Failed to estimate urgent fee: %v", err)
117+
return 0, 0, 0, err
118+
}
119+
120+
urgentFee := btcutil.Amount(float32(urgentConfTargetFee) *
121+
p.UrgentSweepConfTargetFactor)
122+
123+
urgentFeeRate := chainfee.SatPerKWeight(
124+
float32(urgentConfTargetFeeRate) *
125+
p.UrgentSweepConfTargetFactor,
126+
)
127+
log.Infof("Urgent close to expiry fee: %v, fee rate: %v", urgentFee,
128+
urgentFeeRate)
129+
130+
highPrioFee := btcutil.Amount(
131+
float32(amt) / 1_000_000.0 * float32(p.HighPrioFeePPM),
132+
)
133+
if highPrioFee > urgentFee {
134+
log.Infof("High priority fee (%v) exceeding close-to-expiry "+
135+
"fee (%v). Using close-to-expiry fee for sweep: %v, "+
136+
"fee rate: %v", highPrioFee, urgentFee, urgentFee,
137+
urgentFeeRate)
138+
139+
return urgentFee, urgentFeeRate, weight, nil
140+
}
141+
142+
// Find the fee rate for size-relative high priority fee from weight.
143+
highPrioFeeRate := chainfee.NewSatPerKWeight(highPrioFee, weight)
144+
145+
log.Infof("Applying size-relative high priority fee: %v, "+
146+
"fee rate: %v", highPrioFee, highPrioFeeRate)
147+
148+
return highPrioFee, highPrioFeeRate, weight, nil
149+
}
150+
151+
// SweepFeeRate calculates sweep's confTarget and fee in sats.
152+
// It takes high priority case into account.
153+
func (p *SweepFeeProvider) SweepFeeRate(ctx context.Context, amt btcutil.Amount,
154+
addInputToEstimator func(e *input.TxWeightEstimator) error,
155+
destAddr btcutil.Address, cltvExpiry, height int32) (int32,
156+
btcutil.Amount, chainfee.SatPerKWeight, lntypes.WeightUnit, error) {
157+
158+
// Make sure all the numeric fields of the struct are filled.
159+
// If a new field is added, old code might not fill it.
160+
if err := p.Validate(); err != nil {
161+
return 0, 0, 0, 0, err
162+
}
163+
164+
// Get the dynamic sweep confirmation target
165+
confTarget, feeFactor := p.getSweepConfTarget(cltvExpiry, height)
166+
167+
// Make sure fee factor is sane.
168+
if feeFactor == 0 {
169+
return 0, 0, 0, 0, errors.New("fee factor is 0")
170+
}
171+
172+
var (
173+
fee btcutil.Amount
174+
feeRate chainfee.SatPerKWeight
175+
weight lntypes.WeightUnit
176+
err error
177+
)
178+
179+
// If the swap has significant economical size we apply a size-relative
180+
// fee unless the swap is close to expiry which is reflected in the
181+
// UrgentSweepConfTarget.
182+
if amt >= p.HighPrioSwapAmount && confTarget > p.UrgentSweepConfTarget {
183+
fee, feeRate, weight, err = p.getHighPriorityFee(
184+
ctx, addInputToEstimator, destAddr, amt,
185+
)
186+
if err != nil {
187+
return 0, 0, 0, 0, err
188+
}
189+
// FIXME: confTarget variable is irrelevant by this point, but
190+
// it is returned and used for logging. getHighPriorityFee uses
191+
// UrgentSweepConfTarget instead of confTarget or uses the
192+
// proportional approach to fees, not relying on confTarget.
193+
} else {
194+
// If the swap size is below a significant economical size,
195+
// we'll obtain the fee we should use for the swap based on our
196+
// current best weight estimates as well as the confirmation
197+
// target.
198+
fee, feeRate, weight, err = p.Sweeper.GetSweepFeeDetails(
199+
ctx, addInputToEstimator, destAddr, confTarget,
200+
)
201+
if err != nil {
202+
log.Warnf("Failed to estimate fee MuSig2 sweep "+
203+
"txn: %v", err)
204+
return 0, 0, 0, 0, err
205+
}
206+
207+
// Adjust the fee amount and fee rate by the feeFactor which was
208+
// determined in the confirmation target calculation.
209+
fee = btcutil.Amount(float32(fee) * feeFactor)
210+
feeRate = chainfee.SatPerKWeight(float32(feeRate) * feeFactor)
211+
212+
log.Infof("Using fee for a regular priority swap: %v, "+
213+
"fee rate: %v.", fee, feeRate)
214+
}
215+
216+
// Sanity check. Make sure fee rate is not too low.
217+
const minFeeRate = chainfee.AbsoluteFeePerKwFloor
218+
if feeRate < minFeeRate {
219+
log.Infof("Got too low fee rate for the swap: %v. "+
220+
"Increasing it to %v.", feeRate, minFeeRate)
221+
222+
feeRate = minFeeRate
223+
224+
if fee < feeRate.FeeForWeight(weight) {
225+
fee = feeRate.FeeForWeight(weight)
226+
}
227+
}
228+
229+
return confTarget, fee, feeRate, weight, nil
230+
}
231+
232+
// Validate checks that all the numeric fields of the struct are filled.
233+
// If a new field is added, old code might not fill it.
234+
func (p *SweepFeeProvider) Validate() error {
235+
if p.DefaultSweepConfTargetDelta == 0 {
236+
return errors.New("DefaultSweepConfTargetDelta is 0")
237+
}
238+
if p.UrgentSweepConfTarget == 0 {
239+
return errors.New("UrgentSweepConfTarget is 0")
240+
}
241+
if p.DefaultSweepConfTargetFactor == 0 {
242+
return errors.New("DefaultSweepConfTargetFactor is 0")
243+
}
244+
if p.UrgentSweepConfTargetFactor == 0 {
245+
return errors.New("UrgentSweepConfTargetFactor is 0")
246+
}
247+
if p.HighPrioSwapAmount == 0 {
248+
return errors.New("HighPrioSwapAmount is 0")
249+
}
250+
if p.HighPrioFeePPM == 0 {
251+
return errors.New("HighPrioFeePPM is 0")
252+
}
253+
254+
return nil
255+
}

0 commit comments

Comments
 (0)