Skip to content

Commit d5f451a

Browse files
committed
Merge branch '0-19-3-branch-rc1-10125' into 0-19-3-branch-rc1
2 parents 9aa70e4 + 3710bc1 commit d5f451a

File tree

7 files changed

+206
-46
lines changed

7 files changed

+206
-46
lines changed

docs/release-notes/release-notes-0.19.3.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
imported in a watch-only (e.g. remote-signing)
3838
setup](https://github.com/lightningnetwork/lnd/pull/10119).
3939

40+
- [Fixed](https://github.com/lightningnetwork/lnd/pull/10125) a case in the
41+
payment lifecycle where we would retry the same route over and over again in
42+
situations where the sending amount would violate the channel policy
43+
restriction (min,max HTLC).
44+
4045
# New Features
4146

4247
## Functional Enhancements
@@ -90,3 +95,4 @@
9095
* Olaoluwa Osuntokun
9196
* Oliver Gugger
9297
* Yong Yu
98+
* Ziggie

htlcswitch/link.go

Lines changed: 88 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3372,36 +3372,20 @@ func (l *channelLink) canSendHtlc(policy models.ForwardingPolicy,
33723372
heightNow uint32, originalScid lnwire.ShortChannelID,
33733373
customRecords lnwire.CustomRecords) *LinkError {
33743374

3375-
// As our first sanity check, we'll ensure that the passed HTLC isn't
3376-
// too small for the next hop. If so, then we'll cancel the HTLC
3377-
// directly.
3378-
if amt < policy.MinHTLCOut {
3379-
l.log.Warnf("outgoing htlc(%x) is too small: min_htlc=%v, "+
3380-
"htlc_value=%v", payHash[:], policy.MinHTLCOut,
3381-
amt)
3375+
var (
3376+
auxBandwidth OptionalBandwidth
33823377

3383-
// As part of the returned error, we'll send our latest routing
3384-
// policy so the sending node obtains the most up to date data.
3385-
cb := func(upd *lnwire.ChannelUpdate1) lnwire.FailureMessage {
3386-
return lnwire.NewAmountBelowMinimum(amt, *upd)
3387-
}
3388-
failure := l.createFailureWithUpdate(false, originalScid, cb)
3389-
return NewLinkError(failure)
3390-
}
3391-
3392-
// Next, ensure that the passed HTLC isn't too large. If so, we'll
3393-
// cancel the HTLC directly.
3394-
if policy.MaxHTLC != 0 && amt > policy.MaxHTLC {
3395-
l.log.Warnf("outgoing htlc(%x) is too large: max_htlc=%v, "+
3396-
"htlc_value=%v", payHash[:], policy.MaxHTLC, amt)
3378+
// externalErr is an error that is returned by the aux traffic
3379+
// shaper.
3380+
externalErr error
3381+
)
33973382

3398-
// As part of the returned error, we'll send our latest routing
3399-
// policy so the sending node obtains the most up-to-date data.
3400-
cb := func(upd *lnwire.ChannelUpdate1) lnwire.FailureMessage {
3401-
return lnwire.NewTemporaryChannelFailure(upd)
3402-
}
3403-
failure := l.createFailureWithUpdate(false, originalScid, cb)
3404-
return NewDetailedLinkError(failure, OutgoingFailureHTLCExceedsMax)
3383+
// Validate HTLC amount against policy limits.
3384+
linkErr := l.validateHtlcAmount(
3385+
policy, payHash, amt, originalScid, customRecords,
3386+
)
3387+
if linkErr != nil {
3388+
return linkErr
34053389
}
34063390

34073391
// We want to avoid offering an HTLC which will expire in the near
@@ -3416,6 +3400,7 @@ func (l *channelLink) canSendHtlc(policy models.ForwardingPolicy,
34163400
return lnwire.NewExpiryTooSoon(*upd)
34173401
}
34183402
failure := l.createFailureWithUpdate(false, originalScid, cb)
3403+
34193404
return NewLinkError(failure)
34203405
}
34213406

@@ -3431,7 +3416,8 @@ func (l *channelLink) canSendHtlc(policy models.ForwardingPolicy,
34313416
// We now check the available bandwidth to see if this HTLC can be
34323417
// forwarded.
34333418
availableBandwidth := l.Bandwidth()
3434-
auxBandwidth, err := fn.MapOptionZ(
3419+
3420+
auxBandwidth, externalErr = fn.MapOptionZ(
34353421
l.cfg.AuxTrafficShaper,
34363422
func(ts AuxTrafficShaper) fn.Result[OptionalBandwidth] {
34373423
var htlcBlob fn.Option[tlv.Blob]
@@ -3449,8 +3435,10 @@ func (l *channelLink) canSendHtlc(policy models.ForwardingPolicy,
34493435
return l.AuxBandwidth(amt, originalScid, htlcBlob, ts)
34503436
},
34513437
).Unpack()
3452-
if err != nil {
3453-
l.log.Errorf("Unable to determine aux bandwidth: %v", err)
3438+
if externalErr != nil {
3439+
l.log.Errorf("Unable to determine aux bandwidth: %v",
3440+
externalErr)
3441+
34543442
return NewLinkError(&lnwire.FailTemporaryNodeFailure{})
34553443
}
34563444

@@ -3470,6 +3458,7 @@ func (l *channelLink) canSendHtlc(policy models.ForwardingPolicy,
34703458
return lnwire.NewTemporaryChannelFailure(upd)
34713459
}
34723460
failure := l.createFailureWithUpdate(false, originalScid, cb)
3461+
34733462
return NewDetailedLinkError(
34743463
failure, OutgoingFailureInsufficientBalance,
34753464
)
@@ -4579,3 +4568,71 @@ func (l *channelLink) CommitmentCustomBlob() fn.Option[tlv.Blob] {
45794568

45804569
return l.channel.LocalCommitmentBlob()
45814570
}
4571+
4572+
// validateHtlcAmount checks if the HTLC amount is within the policy's
4573+
// minimum and maximum limits. Returns a LinkError if validation fails.
4574+
func (l *channelLink) validateHtlcAmount(policy models.ForwardingPolicy,
4575+
payHash [32]byte, amt lnwire.MilliSatoshi,
4576+
originalScid lnwire.ShortChannelID,
4577+
customRecords lnwire.CustomRecords) *LinkError {
4578+
4579+
// In case we are dealing with a custom HTLC, we don't need to validate
4580+
// the HTLC constraints.
4581+
//
4582+
// NOTE: Custom HTLCs are only locally sourced and will use custom
4583+
// channels which are not routable channels and should have their policy
4584+
// not restricted in the first place. However to be sure we skip this
4585+
// check otherwise we might end up in a loop of sending to the same
4586+
// route again and again because link errors are not persisted in
4587+
// mission control.
4588+
if fn.MapOptionZ(
4589+
l.cfg.AuxTrafficShaper,
4590+
func(ts AuxTrafficShaper) bool {
4591+
return ts.IsCustomHTLC(customRecords)
4592+
},
4593+
) {
4594+
4595+
l.log.Debugf("skipping htlc amount policy validation for " +
4596+
"custom htlc")
4597+
4598+
return nil
4599+
}
4600+
4601+
// As our first sanity check, we'll ensure that the passed HTLC isn't
4602+
// too small for the next hop. If so, then we'll cancel the HTLC
4603+
// directly.
4604+
if amt < policy.MinHTLCOut {
4605+
l.log.Warnf("outgoing htlc(%x) is too small: min_htlc=%v, "+
4606+
"htlc_value=%v", payHash[:], policy.MinHTLCOut,
4607+
amt)
4608+
4609+
// As part of the returned error, we'll send our latest routing
4610+
// policy so the sending node obtains the most up to date data.
4611+
cb := func(upd *lnwire.ChannelUpdate1) lnwire.FailureMessage {
4612+
return lnwire.NewAmountBelowMinimum(amt, *upd)
4613+
}
4614+
failure := l.createFailureWithUpdate(false, originalScid, cb)
4615+
4616+
return NewLinkError(failure)
4617+
}
4618+
4619+
// Next, ensure that the passed HTLC isn't too large. If so, we'll
4620+
// cancel the HTLC directly.
4621+
if policy.MaxHTLC != 0 && amt > policy.MaxHTLC {
4622+
l.log.Warnf("outgoing htlc(%x) is too large: max_htlc=%v, "+
4623+
"htlc_value=%v", payHash[:], policy.MaxHTLC, amt)
4624+
4625+
// As part of the returned error, we'll send our latest routing
4626+
// policy so the sending node obtains the most up-to-date data.
4627+
cb := func(upd *lnwire.ChannelUpdate1) lnwire.FailureMessage {
4628+
return lnwire.NewTemporaryChannelFailure(upd)
4629+
}
4630+
failure := l.createFailureWithUpdate(false, originalScid, cb)
4631+
4632+
return NewDetailedLinkError(
4633+
failure, OutgoingFailureHTLCExceedsMax,
4634+
)
4635+
}
4636+
4637+
return nil
4638+
}

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@ var allTestCases = []*lntest.TestCase{
238238
Name: "wumbo channels",
239239
TestFunc: testWumboChannels,
240240
},
241+
{
242+
Name: "max htlc path payment",
243+
TestFunc: testMaxHtlcPathPayment,
244+
},
241245
{
242246
Name: "max htlc pathfind",
243247
TestFunc: testMaxHtlcPathfind,

itest/lnd_max_htlc_path_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package itest
2+
3+
import (
4+
"github.com/lightningnetwork/lnd/lnrpc"
5+
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
6+
"github.com/lightningnetwork/lnd/lntest"
7+
)
8+
9+
// testMaxHtlcPathPayment tests that when a payment is attempted, the path
10+
// finding logic correctly takes into account the max_htlc value of the first
11+
// channel.
12+
func testMaxHtlcPathPayment(ht *lntest.HarnessTest) {
13+
// Create a channel Alice->Bob.
14+
chanPoints, nodes := ht.CreateSimpleNetwork(
15+
[][]string{nil, nil}, lntest.OpenChannelParams{
16+
Amt: 1000000,
17+
},
18+
)
19+
alice, bob := nodes[0], nodes[1]
20+
chanPoint := chanPoints[0]
21+
22+
// Alice and Bob should have one channel open with each other now.
23+
ht.AssertNodeNumChannels(alice, 1)
24+
ht.AssertNodeNumChannels(bob, 1)
25+
26+
// Define a max_htlc value that is lower than the default.
27+
const (
28+
maxHtlcMsat = 50000000
29+
minHtlcMsat = 1000
30+
timeLockDelta = 80
31+
baseFeeMsat = 0
32+
feeRate = 0
33+
)
34+
35+
// Update Alice's channel policy to set the new max_htlc value.
36+
req := &lnrpc.PolicyUpdateRequest{
37+
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
38+
ChanPoint: chanPoint,
39+
},
40+
MaxHtlcMsat: maxHtlcMsat,
41+
MinHtlcMsat: minHtlcMsat,
42+
BaseFeeMsat: baseFeeMsat,
43+
FeeRate: feeRate,
44+
TimeLockDelta: timeLockDelta,
45+
}
46+
alice.RPC.UpdateChannelPolicy(req)
47+
48+
expectedPolicy := &lnrpc.RoutingPolicy{
49+
FeeBaseMsat: baseFeeMsat,
50+
FeeRateMilliMsat: feeRate,
51+
TimeLockDelta: timeLockDelta,
52+
MinHtlc: minHtlcMsat,
53+
MaxHtlcMsat: maxHtlcMsat,
54+
}
55+
56+
// Wait for the policy update to propagate to Bob.
57+
ht.AssertChannelPolicyUpdate(
58+
bob, alice, expectedPolicy, chanPoint, false,
59+
)
60+
61+
// Create an invoice for an amount greater than the max htlc value.
62+
invoiceAmt := int64(maxHtlcMsat + 10_000_000)
63+
invoice := &lnrpc.Invoice{ValueMsat: invoiceAmt}
64+
resp := bob.RPC.AddInvoice(invoice)
65+
66+
// Attempt to pay the invoice from Alice. The payment should be
67+
// splitted into two parts, one for the max_htlc value and one for the
68+
// remaining amount and succeed.
69+
payReq := &routerrpc.SendPaymentRequest{
70+
PaymentRequest: resp.PaymentRequest,
71+
FeeLimitMsat: noFeeLimitMsat,
72+
}
73+
ht.SendPaymentAssertSettled(alice, payReq)
74+
}

routing/bandwidth.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ type bandwidthHints interface {
2424
availableChanBandwidth(channelID uint64,
2525
amount lnwire.MilliSatoshi) (lnwire.MilliSatoshi, bool)
2626

27-
// firstHopCustomBlob returns the custom blob for the first hop of the
28-
// payment, if available.
29-
firstHopCustomBlob() fn.Option[tlv.Blob]
27+
// isCustomHTLCPayment returns true if this payment is a custom payment.
28+
// For custom payments policy checks might not be needed.
29+
isCustomHTLCPayment() bool
3030
}
3131

3232
// getLinkQuery is the function signature used to lookup a link.
@@ -205,8 +205,27 @@ func (b *bandwidthManager) availableChanBandwidth(channelID uint64,
205205
return bandwidth, true
206206
}
207207

208-
// firstHopCustomBlob returns the custom blob for the first hop of the payment,
209-
// if available.
210-
func (b *bandwidthManager) firstHopCustomBlob() fn.Option[tlv.Blob] {
211-
return b.firstHopBlob
208+
// isCustomHTLCPayment returns true if this payment is a custom payment.
209+
// For custom payments policy checks might not be needed.
210+
func (b *bandwidthManager) isCustomHTLCPayment() bool {
211+
var isCustomHTLCPayment bool
212+
213+
b.firstHopBlob.WhenSome(func(blob tlv.Blob) {
214+
customRecords, err := lnwire.ParseCustomRecords(blob)
215+
if err != nil {
216+
log.Warnf("failed to parse custom records when "+
217+
"checking if payment is custom: %v", err)
218+
219+
return
220+
}
221+
222+
isCustomHTLCPayment = fn.MapOptionZ(
223+
b.trafficShaper,
224+
func(s htlcswitch.AuxTrafficShaper) bool {
225+
return s.IsCustomHTLC(customRecords)
226+
},
227+
)
228+
})
229+
230+
return isCustomHTLCPayment
212231
}

routing/integrated_routing_context_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/lightningnetwork/lnd/kvdb"
1212
"github.com/lightningnetwork/lnd/lnwire"
1313
"github.com/lightningnetwork/lnd/routing/route"
14-
"github.com/lightningnetwork/lnd/tlv"
1514
"github.com/lightningnetwork/lnd/zpay32"
1615
"github.com/stretchr/testify/require"
1716
)
@@ -36,8 +35,8 @@ func (m *mockBandwidthHints) availableChanBandwidth(channelID uint64,
3635
return balance, ok
3736
}
3837

39-
func (m *mockBandwidthHints) firstHopCustomBlob() fn.Option[tlv.Blob] {
40-
return fn.None[tlv.Blob]()
38+
func (m *mockBandwidthHints) isCustomHTLCPayment() bool {
39+
return false
4140
}
4241

4342
// integratedRoutingContext defines the context in which integrated routing

routing/unified_edges.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,13 @@ func (u *edgeUnifier) getEdgeLocal(netAmtReceived lnwire.MilliSatoshi,
251251
// Add inbound fee to get to the amount that is sent over the
252252
// local channel.
253253
amt := netAmtReceived + lnwire.MilliSatoshi(inboundFee)
254-
255254
// Check valid amount range for the channel. We skip this test
256-
// for payments with custom HTLC data, as the amount sent on
257-
// the BTC layer may differ from the amount that is actually
258-
// forwarded in custom channels.
259-
if bandwidthHints.firstHopCustomBlob().IsNone() &&
255+
256+
// for payments with custom htlc data we skip the amount range
257+
// check because the amt of the payment does not relate to the
258+
// actual amount carried by the HTLC but instead in encoded in
259+
// the blob data.
260+
if !bandwidthHints.isCustomHTLCPayment() &&
260261
!edge.amtInRange(amt) {
261262

262263
log.Debugf("Amount %v not in range for edge %v",

0 commit comments

Comments
 (0)