Skip to content

Commit b269635

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

File tree

3 files changed

+697
-0
lines changed

3 files changed

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

0 commit comments

Comments
 (0)