@@ -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
0 commit comments