Skip to content

Commit 7f809c7

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

File tree

3 files changed

+732
-0
lines changed

3 files changed

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

0 commit comments

Comments
 (0)