Skip to content

Commit ac4ef28

Browse files
authored
Merge pull request #8824 from Crypt-iQ/precise_dust_limit
multi: include commitment fees in dust calculation
2 parents 67c5fa9 + 74636e9 commit ac4ef28

File tree

13 files changed

+461
-89
lines changed

13 files changed

+461
-89
lines changed

config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ type Config struct {
441441

442442
GcCanceledInvoicesOnTheFly bool `long:"gc-canceled-invoices-on-the-fly" description:"If true, we'll delete newly canceled invoices on the fly."`
443443

444-
DustThreshold uint64 `long:"dust-threshold" description:"Sets the dust sum threshold in satoshis for a channel after which dust HTLC's will be failed."`
444+
MaxFeeExposure uint64 `long:"dust-threshold" description:"Sets the max fee exposure in satoshis for a channel after which HTLC's will be failed."`
445445

446446
Fee *lncfg.Fee `group:"fee" namespace:"fee"`
447447

@@ -693,7 +693,7 @@ func DefaultConfig() Config {
693693
MaxOutgoingCltvExpiry: htlcswitch.DefaultMaxOutgoingCltvExpiry,
694694
MaxChannelFeeAllocation: htlcswitch.DefaultMaxLinkFeeAllocation,
695695
MaxCommitFeeRateAnchors: lnwallet.DefaultAnchorsCommitMaxFeeRateSatPerVByte,
696-
DustThreshold: uint64(htlcswitch.DefaultDustThreshold.ToSatoshis()),
696+
MaxFeeExposure: uint64(htlcswitch.DefaultMaxFeeExposure.ToSatoshis()),
697697
LogWriter: build.NewRotatingLogWriter(),
698698
DB: lncfg.DefaultDB(),
699699
Cluster: lncfg.DefaultCluster(),

docs/release-notes/release-notes-0.18.3.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ commitment when the channel was force closed.
8484
All units are `sats/kvB`. If the new field `min_relay_feerate` is not set,
8585
the default floor feerate (1012 sats/kvB) will be used.
8686

87+
* Commitment fees are now taken into account when [calculating the fee
88+
exposure threshold](https://github.com/lightningnetwork/lnd/pull/8824).
89+
8790
## RPC Updates
8891

8992
* [`xImportMissionControl`](https://github.com/lightningnetwork/lnd/pull/8779)
@@ -161,6 +164,7 @@ commitment when the channel was force closed.
161164
* bitromortac
162165
* Bufo
163166
* Elle Mouton
167+
* Eugene Siegel
164168
* Matheus Degiovani
165169
* Oliver Gugger
166170
* Slyghtning

htlcswitch/interfaces.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package htlcswitch
33
import (
44
"context"
55

6+
"github.com/btcsuite/btcd/btcutil"
67
"github.com/btcsuite/btcd/wire"
78
"github.com/lightningnetwork/lnd/channeldb"
89
"github.com/lightningnetwork/lnd/channeldb/models"
10+
"github.com/lightningnetwork/lnd/fn"
911
"github.com/lightningnetwork/lnd/invoices"
1012
"github.com/lightningnetwork/lnd/lntypes"
1113
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
@@ -59,15 +61,21 @@ type packetHandler interface {
5961
// whether a link has too much dust exposure.
6062
type dustHandler interface {
6163
// getDustSum returns the dust sum on either the local or remote
62-
// commitment.
63-
getDustSum(remote bool) lnwire.MilliSatoshi
64+
// commitment. An optional fee parameter can be passed in which is used
65+
// to calculate the dust sum.
66+
getDustSum(remote bool,
67+
fee fn.Option[chainfee.SatPerKWeight]) lnwire.MilliSatoshi
6468

6569
// getFeeRate returns the current channel feerate.
6670
getFeeRate() chainfee.SatPerKWeight
6771

6872
// getDustClosure returns a closure that can evaluate whether a passed
6973
// HTLC is dust.
7074
getDustClosure() dustClosure
75+
76+
// getCommitFee returns the commitment fee in satoshis from either the
77+
// local or remote commitment. This does not include dust.
78+
getCommitFee(remote bool) btcutil.Amount
7179
}
7280

7381
// scidAliasHandler is an interface that the ChannelLink implements so it can

htlcswitch/link.go

Lines changed: 201 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/lightningnetwork/lnd/fn"
2222
"github.com/lightningnetwork/lnd/htlcswitch/hodl"
2323
"github.com/lightningnetwork/lnd/htlcswitch/hop"
24+
"github.com/lightningnetwork/lnd/input"
2425
"github.com/lightningnetwork/lnd/invoices"
2526
"github.com/lightningnetwork/lnd/lnpeer"
2627
"github.com/lightningnetwork/lnd/lntypes"
@@ -278,6 +279,10 @@ type ChannelLinkConfig struct {
278279
// by failing back any blinding-related payloads as if they were
279280
// invalid.
280281
DisallowRouteBlinding bool
282+
283+
// MaxFeeExposure is the threshold in milli-satoshis after which we'll
284+
// restrict the flow of HTLCs and fee updates.
285+
MaxFeeExposure lnwire.MilliSatoshi
281286
}
282287

283288
// channelLink is the service which drives a channel's commitment update
@@ -447,6 +452,11 @@ func NewChannelLink(cfg ChannelLinkConfig,
447452

448453
logPrefix := fmt.Sprintf("ChannelLink(%v):", channel.ChannelPoint())
449454

455+
// If the max fee exposure isn't set, use the default.
456+
if cfg.MaxFeeExposure == 0 {
457+
cfg.MaxFeeExposure = DefaultMaxFeeExposure
458+
}
459+
450460
return &channelLink{
451461
cfg: cfg,
452462
channel: channel,
@@ -1591,6 +1601,20 @@ func (l *channelLink) handleDownstreamUpdateAdd(pkt *htlcPacket) error {
15911601
return nil
15921602
}
15931603

1604+
// Check if we can add the HTLC here without exceededing the max fee
1605+
// exposure threshold.
1606+
if l.isOverexposedWithHtlc(htlc, false) {
1607+
l.log.Debugf("Unable to handle downstream HTLC - max fee " +
1608+
"exposure exceeded")
1609+
1610+
l.mailBox.FailAdd(pkt)
1611+
1612+
return NewDetailedLinkError(
1613+
lnwire.NewTemporaryChannelFailure(nil),
1614+
OutgoingFailureDownstreamHtlcAdd,
1615+
)
1616+
}
1617+
15941618
// A new payment has been initiated via the downstream channel,
15951619
// so we add the new HTLC to our local log, then update the
15961620
// commitment chains.
@@ -1958,6 +1982,18 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) {
19581982
return
19591983
}
19601984

1985+
// We have to check the limit here rather than later in the
1986+
// switch because the counterparty can keep sending HTLC's
1987+
// without sending a revoke. This would mean that the switch
1988+
// check would only occur later.
1989+
if l.isOverexposedWithHtlc(msg, true) {
1990+
l.fail(LinkFailureError{code: ErrInternalError},
1991+
"peer sent us an HTLC that exceeded our max "+
1992+
"fee exposure")
1993+
1994+
return
1995+
}
1996+
19611997
// We just received an add request from an upstream peer, so we
19621998
// add it to our state machine, then add the HTLC to our
19631999
// "settle" list in the event that we know the preimage.
@@ -2375,9 +2411,32 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) {
23752411
l.RWMutex.Unlock()
23762412

23772413
case *lnwire.UpdateFee:
2414+
// Check and see if their proposed fee-rate would make us
2415+
// exceed the fee threshold.
2416+
fee := chainfee.SatPerKWeight(msg.FeePerKw)
2417+
2418+
isDust, err := l.exceedsFeeExposureLimit(fee)
2419+
if err != nil {
2420+
// This shouldn't typically happen. If it does, it
2421+
// indicates something is wrong with our channel state.
2422+
l.log.Errorf("Unable to determine if fee threshold " +
2423+
"exceeded")
2424+
l.fail(LinkFailureError{code: ErrInternalError},
2425+
"error calculating fee exposure: %v", err)
2426+
2427+
return
2428+
}
2429+
2430+
if isDust {
2431+
// The proposed fee-rate makes us exceed the fee
2432+
// threshold.
2433+
l.fail(LinkFailureError{code: ErrInternalError},
2434+
"fee threshold exceeded: %v", err)
2435+
return
2436+
}
2437+
23782438
// We received fee update from peer. If we are the initiator we
23792439
// will fail the channel, if not we will apply the update.
2380-
fee := chainfee.SatPerKWeight(msg.FeePerKw)
23812440
if err := l.channel.ReceiveUpdateFee(fee); err != nil {
23822441
l.fail(LinkFailureError{code: ErrInvalidUpdate},
23832442
"error receiving fee update: %v", err)
@@ -2668,8 +2727,10 @@ func (l *channelLink) MayAddOutgoingHtlc(amt lnwire.MilliSatoshi) error {
26682727
// method.
26692728
//
26702729
// NOTE: Part of the dustHandler interface.
2671-
func (l *channelLink) getDustSum(remote bool) lnwire.MilliSatoshi {
2672-
return l.channel.GetDustSum(remote)
2730+
func (l *channelLink) getDustSum(remote bool,
2731+
dryRunFee fn.Option[chainfee.SatPerKWeight]) lnwire.MilliSatoshi {
2732+
2733+
return l.channel.GetDustSum(remote, dryRunFee)
26732734
}
26742735

26752736
// getFeeRate is a wrapper method that retrieves the underlying channel's
@@ -2692,6 +2753,130 @@ func (l *channelLink) getDustClosure() dustClosure {
26922753
return dustHelper(chanType, localDustLimit, remoteDustLimit)
26932754
}
26942755

2756+
// getCommitFee returns either the local or remote CommitFee in satoshis. This
2757+
// is used so that the Switch can have access to the commitment fee without
2758+
// needing to have a *LightningChannel. This doesn't include dust.
2759+
//
2760+
// NOTE: Part of the dustHandler interface.
2761+
func (l *channelLink) getCommitFee(remote bool) btcutil.Amount {
2762+
if remote {
2763+
return l.channel.State().RemoteCommitment.CommitFee
2764+
}
2765+
2766+
return l.channel.State().LocalCommitment.CommitFee
2767+
}
2768+
2769+
// exceedsFeeExposureLimit returns whether or not the new proposed fee-rate
2770+
// increases the total dust and fees within the channel past the configured
2771+
// fee threshold. It first calculates the dust sum over every update in the
2772+
// update log with the proposed fee-rate and taking into account both the local
2773+
// and remote dust limits. It uses every update in the update log instead of
2774+
// what is actually on the local and remote commitments because it is assumed
2775+
// that in a worst-case scenario, every update in the update log could
2776+
// theoretically be on either commitment transaction and this needs to be
2777+
// accounted for with this fee-rate. It then calculates the local and remote
2778+
// commitment fees given the proposed fee-rate. Finally, it tallies the results
2779+
// and determines if the fee threshold has been exceeded.
2780+
func (l *channelLink) exceedsFeeExposureLimit(
2781+
feePerKw chainfee.SatPerKWeight) (bool, error) {
2782+
2783+
dryRunFee := fn.Some[chainfee.SatPerKWeight](feePerKw)
2784+
2785+
// Get the sum of dust for both the local and remote commitments using
2786+
// this "dry-run" fee.
2787+
localDustSum := l.getDustSum(false, dryRunFee)
2788+
remoteDustSum := l.getDustSum(true, dryRunFee)
2789+
2790+
// Calculate the local and remote commitment fees using this dry-run
2791+
// fee.
2792+
localFee, remoteFee, err := l.channel.CommitFeeTotalAt(feePerKw)
2793+
if err != nil {
2794+
return false, err
2795+
}
2796+
2797+
// Finally, check whether the max fee exposure was exceeded on either
2798+
// future commitment transaction with the fee-rate.
2799+
totalLocalDust := localDustSum + lnwire.NewMSatFromSatoshis(localFee)
2800+
if totalLocalDust > l.cfg.MaxFeeExposure {
2801+
return true, nil
2802+
}
2803+
2804+
totalRemoteDust := remoteDustSum + lnwire.NewMSatFromSatoshis(
2805+
remoteFee,
2806+
)
2807+
2808+
return totalRemoteDust > l.cfg.MaxFeeExposure, nil
2809+
}
2810+
2811+
// isOverexposedWithHtlc calculates whether the proposed HTLC will make the
2812+
// channel exceed the fee threshold. It first fetches the largest fee-rate that
2813+
// may be on any unrevoked commitment transaction. Then, using this fee-rate,
2814+
// determines if the to-be-added HTLC is dust. If the HTLC is dust, it adds to
2815+
// the overall dust sum. If it is not dust, it contributes to weight, which
2816+
// also adds to the overall dust sum by an increase in fees. If the dust sum on
2817+
// either commitment exceeds the configured fee threshold, this function
2818+
// returns true.
2819+
func (l *channelLink) isOverexposedWithHtlc(htlc *lnwire.UpdateAddHTLC,
2820+
incoming bool) bool {
2821+
2822+
dustClosure := l.getDustClosure()
2823+
2824+
feeRate := l.channel.WorstCaseFeeRate()
2825+
2826+
amount := htlc.Amount.ToSatoshis()
2827+
2828+
// See if this HTLC is dust on both the local and remote commitments.
2829+
isLocalDust := dustClosure(feeRate, incoming, true, amount)
2830+
isRemoteDust := dustClosure(feeRate, incoming, false, amount)
2831+
2832+
// Calculate the dust sum for the local and remote commitments.
2833+
localDustSum := l.getDustSum(false, fn.None[chainfee.SatPerKWeight]())
2834+
remoteDustSum := l.getDustSum(true, fn.None[chainfee.SatPerKWeight]())
2835+
2836+
// Grab the larger of the local and remote commitment fees w/o dust.
2837+
commitFee := l.getCommitFee(false)
2838+
2839+
if l.getCommitFee(true) > commitFee {
2840+
commitFee = l.getCommitFee(true)
2841+
}
2842+
2843+
localDustSum += lnwire.NewMSatFromSatoshis(commitFee)
2844+
remoteDustSum += lnwire.NewMSatFromSatoshis(commitFee)
2845+
2846+
// Calculate the additional fee increase if this is a non-dust HTLC.
2847+
weight := lntypes.WeightUnit(input.HTLCWeight)
2848+
additional := lnwire.NewMSatFromSatoshis(
2849+
feeRate.FeeForWeight(weight),
2850+
)
2851+
2852+
if isLocalDust {
2853+
// If this is dust, it doesn't contribute to weight but does
2854+
// contribute to the overall dust sum.
2855+
localDustSum += lnwire.NewMSatFromSatoshis(amount)
2856+
} else {
2857+
// Account for the fee increase that comes with an increase in
2858+
// weight.
2859+
localDustSum += additional
2860+
}
2861+
2862+
if localDustSum > l.cfg.MaxFeeExposure {
2863+
// The max fee exposure was exceeded.
2864+
return true
2865+
}
2866+
2867+
if isRemoteDust {
2868+
// If this is dust, it doesn't contribute to weight but does
2869+
// contribute to the overall dust sum.
2870+
remoteDustSum += lnwire.NewMSatFromSatoshis(amount)
2871+
} else {
2872+
// Account for the fee increase that comes with an increase in
2873+
// weight.
2874+
remoteDustSum += additional
2875+
}
2876+
2877+
return remoteDustSum > l.cfg.MaxFeeExposure
2878+
}
2879+
26952880
// dustClosure is a function that evaluates whether an HTLC is dust. It returns
26962881
// true if the HTLC is dust. It takes in a feerate, a boolean denoting whether
26972882
// the HTLC is incoming (i.e. one that the remote sent), a boolean denoting
@@ -3060,6 +3245,19 @@ func (l *channelLink) updateChannelFee(feePerKw chainfee.SatPerKWeight) error {
30603245
return nil
30613246
}
30623247

3248+
// Check and see if our proposed fee-rate would make us exceed the fee
3249+
// threshold.
3250+
thresholdExceeded, err := l.exceedsFeeExposureLimit(feePerKw)
3251+
if err != nil {
3252+
// This shouldn't typically happen. If it does, it indicates
3253+
// something is wrong with our channel state.
3254+
return err
3255+
}
3256+
3257+
if thresholdExceeded {
3258+
return fmt.Errorf("link fee threshold exceeded")
3259+
}
3260+
30633261
// First, we'll update the local fee on our commitment.
30643262
if err := l.channel.UpdateFee(feePerKw); err != nil {
30653263
return err

htlcswitch/link_test.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4471,10 +4471,20 @@ func TestChannelLinkUpdateCommitFee(t *testing.T) {
44714471

44724472
// Triggering the link to update the fee of the channel with a fee rate
44734473
// that exceeds its maximum fee allocation should result in a fee rate
4474-
// corresponding to the maximum fee allocation.
4474+
// corresponding to the maximum fee allocation. Increase the dust
4475+
// threshold so that we don't trigger that logic.
4476+
highFeeExposure := lnwire.NewMSatFromSatoshis(
4477+
2 * btcutil.SatoshiPerBitcoin,
4478+
)
44754479
const maxFeeRate chainfee.SatPerKWeight = 207180182
4480+
n.aliceChannelLink.cfg.MaxFeeExposure = highFeeExposure
4481+
n.firstBobChannelLink.cfg.MaxFeeExposure = highFeeExposure
44764482
triggerFeeUpdate(maxFeeRate+1, minRelayFee, maxFeeRate, true)
44774483

4484+
// Decrease the max fee exposure back to normal.
4485+
n.aliceChannelLink.cfg.MaxFeeExposure = DefaultMaxFeeExposure
4486+
n.firstBobChannelLink.cfg.MaxFeeExposure = DefaultMaxFeeExposure
4487+
44784488
// Triggering the link to update the fee of the channel with a fee rate
44794489
// that is below the current min relay fee rate should result in a fee
44804490
// rate corresponding to the minimum relay fee.

0 commit comments

Comments
 (0)