Skip to content

Commit a4eb9d7

Browse files
committed
htlcswitch: use evaluateDustThreshold in SendHTLC, handlePacketForward
This commit makes SendHTLC (we are the source) evaluate the dust threshold of the outgoing channel against the default threshold of 500K satoshis. If the threshold is exceeded by adding this HTLC, we fail backwards. It also makes handlePacketForward (we are forwarding) evaluate the dust threshold of the incoming channel and the outgoing channel and fails backwards if either channel's dust sum exceeds the default threshold. This helps mitigate the dust griefing attack.
1 parent 1877d05 commit a4eb9d7

File tree

2 files changed

+464
-21
lines changed

2 files changed

+464
-21
lines changed

htlcswitch/switch.go

Lines changed: 166 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ var (
7171
// ErrLocalAddFailed signals that the ADD htlc for a local payment
7272
// failed to be processed.
7373
ErrLocalAddFailed = errors.New("local add HTLC failed")
74+
75+
// errDustThresholdExceeded is only surfaced to callers of SendHTLC and
76+
// signals that sending the HTLC would exceed the outgoing link's dust
77+
// threshold.
78+
errDustThresholdExceeded = errors.New("dust threshold exceeded")
79+
80+
// defaultDustThreshold is the default threshold after which we'll fail
81+
// payments if they are dust. This is currently set to 500k sats.
82+
defaultDustThreshold = lnwire.MilliSatoshi(500_000_000)
7483
)
7584

7685
// plexPacket encapsulates switch packet and adds error channel to receive
@@ -469,6 +478,51 @@ func (s *Switch) SendHTLC(firstHop lnwire.ShortChannelID, attemptID uint64,
469478
htlc: htlc,
470479
}
471480

481+
// Attempt to fetch the target link before creating a circuit so that
482+
// we don't leave dangling circuits. The getLocalLink method does not
483+
// require the circuit variable to be set on the *htlcPacket.
484+
link, linkErr := s.getLocalLink(packet, htlc)
485+
if linkErr != nil {
486+
// Notify the htlc notifier of a link failure on our outgoing
487+
// link. Incoming timelock/amount values are not set because
488+
// they are not present for local sends.
489+
s.cfg.HtlcNotifier.NotifyLinkFailEvent(
490+
newHtlcKey(packet),
491+
HtlcInfo{
492+
OutgoingTimeLock: htlc.Expiry,
493+
OutgoingAmt: htlc.Amount,
494+
},
495+
HtlcEventTypeSend,
496+
linkErr,
497+
false,
498+
)
499+
500+
return linkErr
501+
}
502+
503+
// Evaluate whether this HTLC would increase our exposure to dust. If
504+
// it does, don't send it out and instead return an error.
505+
if s.evaluateDustThreshold(link, htlc.Amount, false) {
506+
// Notify the htlc notifier of a link failure on our outgoing
507+
// link. We use the FailTemporaryChannelFailure in place of a
508+
// more descriptive error message.
509+
linkErr := NewLinkError(
510+
&lnwire.FailTemporaryChannelFailure{},
511+
)
512+
s.cfg.HtlcNotifier.NotifyLinkFailEvent(
513+
newHtlcKey(packet),
514+
HtlcInfo{
515+
OutgoingTimeLock: htlc.Expiry,
516+
OutgoingAmt: htlc.Amount,
517+
},
518+
HtlcEventTypeSend,
519+
linkErr,
520+
false,
521+
)
522+
523+
return errDustThresholdExceeded
524+
}
525+
472526
circuit := newPaymentCircuit(&htlc.PaymentHash, packet)
473527
actions, err := s.circuits.CommitCircuits(circuit)
474528
if err != nil {
@@ -488,27 +542,6 @@ func (s *Switch) SendHTLC(firstHop lnwire.ShortChannelID, attemptID uint64,
488542
// Send packet to link.
489543
packet.circuit = circuit
490544

491-
// User has created the htlc update therefore we should find the
492-
// appropriate channel link and send the payment over this link.
493-
link, linkErr := s.getLocalLink(packet, htlc)
494-
if linkErr != nil {
495-
// Notify the htlc notifier of a link failure on our
496-
// outgoing link. Incoming timelock/amount values are
497-
// not set because they are not present for local sends.
498-
s.cfg.HtlcNotifier.NotifyLinkFailEvent(
499-
newHtlcKey(packet),
500-
HtlcInfo{
501-
OutgoingTimeLock: htlc.Expiry,
502-
OutgoingAmt: htlc.Amount,
503-
},
504-
HtlcEventTypeSend,
505-
linkErr,
506-
false,
507-
)
508-
509-
return linkErr
510-
}
511-
512545
return link.handleLocalAddPacket(packet)
513546
}
514547

@@ -1098,6 +1131,52 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
10981131
// what the best channel is.
10991132
destination := destinations[rand.Intn(len(destinations))]
11001133

1134+
// Retrieve the incoming link by its ShortChannelID. Note that
1135+
// the incomingChanID is never set to hop.Source here.
1136+
s.indexMtx.RLock()
1137+
incomingLink, err := s.getLinkByShortID(packet.incomingChanID)
1138+
if err != nil {
1139+
s.indexMtx.RUnlock()
1140+
1141+
// If we couldn't find the incoming link, we can't
1142+
// evaluate the incoming's exposure to dust, so we just
1143+
// fail the HTLC back.
1144+
linkErr := NewLinkError(
1145+
&lnwire.FailTemporaryChannelFailure{},
1146+
)
1147+
1148+
return s.failAddPacket(packet, linkErr)
1149+
}
1150+
s.indexMtx.RUnlock()
1151+
1152+
// Evaluate whether this HTLC would increase our exposure to
1153+
// dust on the incoming link. If it does, fail it backwards.
1154+
if s.evaluateDustThreshold(
1155+
incomingLink, packet.incomingAmount, true,
1156+
) {
1157+
// The incoming dust exceeds the threshold, so we fail
1158+
// the add back.
1159+
linkErr := NewLinkError(
1160+
&lnwire.FailTemporaryChannelFailure{},
1161+
)
1162+
1163+
return s.failAddPacket(packet, linkErr)
1164+
}
1165+
1166+
// Also evaluate whether this HTLC would increase our exposure
1167+
// to dust on the destination link. If it does, fail it back.
1168+
if s.evaluateDustThreshold(
1169+
destination, packet.amount, false,
1170+
) {
1171+
// The outgoing dust exceeds the threshold, so we fail
1172+
// the add back.
1173+
linkErr := NewLinkError(
1174+
&lnwire.FailTemporaryChannelFailure{},
1175+
)
1176+
1177+
return s.failAddPacket(packet, linkErr)
1178+
}
1179+
11011180
// Send the packet to the destination channel link which
11021181
// manages the channel.
11031182
packet.outgoingChanID = destination.ShortChanID()
@@ -2279,3 +2358,69 @@ func (s *Switch) FlushForwardingEvents() error {
22792358
func (s *Switch) BestHeight() uint32 {
22802359
return atomic.LoadUint32(&s.bestHeight)
22812360
}
2361+
2362+
// evaluateDustThreshold takes in a ChannelLink, HTLC amount, and a boolean to
2363+
// determine whether the default dust threshold has been exceeded. This
2364+
// heuristic is only used for HTLC's that are below either side's dust limits.
2365+
// The sum of the commitment's dust with the mailbox's dust with the amount is
2366+
// checked against the default threshold. If incoming is true, then the amount
2367+
// is not included in the sum as it was already included in the commitment's
2368+
// dust. A boolean is returned telling the caller whether the HTLC should be
2369+
// failed back.
2370+
func (s *Switch) evaluateDustThreshold(link ChannelLink,
2371+
amount lnwire.MilliSatoshi, incoming bool) bool {
2372+
2373+
// Retrieve the link's local and remote dust limits.
2374+
localLimit, remoteLimit := link.getDustLimits()
2375+
2376+
// Now check whether this HTLC amount is dust on either side's
2377+
// commitment transaction.
2378+
isLocalDust := amount < localLimit
2379+
isRemoteDust := amount < remoteLimit
2380+
if isLocalDust || isRemoteDust {
2381+
// Fetch the dust sums currently in the mailbox for this link.
2382+
cid := link.ChanID()
2383+
sid := link.ShortChanID()
2384+
mailbox := s.mailOrchestrator.GetOrCreateMailBox(cid, sid)
2385+
localMailDust, remoteMailDust := mailbox.DustPackets()
2386+
2387+
// If the htlc is dust on the local commitment, we'll obtain
2388+
// the dust sum for it.
2389+
if isLocalDust {
2390+
localSum := link.getDustSum(false)
2391+
localSum += localMailDust
2392+
2393+
// Optionally include the HTLC amount only for outgoing
2394+
// channels.
2395+
if !incoming {
2396+
localSum += amount
2397+
}
2398+
2399+
// Finally check against the defined dust threshold.
2400+
if localSum > defaultDustThreshold {
2401+
return true
2402+
}
2403+
}
2404+
2405+
// Also check if the htlc is dust on the remote commitment, if
2406+
// we've reached this point.
2407+
if isRemoteDust {
2408+
remoteSum := link.getDustSum(true)
2409+
remoteSum += remoteMailDust
2410+
2411+
// Optionally include the HTLC amount only for outgoing
2412+
// channels.
2413+
if !incoming {
2414+
remoteSum += amount
2415+
}
2416+
2417+
// Finally check against the defined dust threshold.
2418+
if remoteSum > defaultDustThreshold {
2419+
return true
2420+
}
2421+
}
2422+
}
2423+
2424+
// If we reached this point, this HTLC is fine to forward.
2425+
return false
2426+
}

0 commit comments

Comments
 (0)