multi: skip range check in pathfinder and switch for custom htlc payments

This commit is contained in:
ziggie
2025-08-02 17:34:23 +02:00
parent c4db070e5f
commit e2a9b17254
4 changed files with 112 additions and 48 deletions

View File

@@ -2547,36 +2547,12 @@ func (l *channelLink) canSendHtlc(policy models.ForwardingPolicy,
heightNow uint32, originalScid lnwire.ShortChannelID, heightNow uint32, originalScid lnwire.ShortChannelID,
customRecords lnwire.CustomRecords) *LinkError { customRecords lnwire.CustomRecords) *LinkError {
// As our first sanity check, we'll ensure that the passed HTLC isn't // Validate HTLC amount against policy limits.
// too small for the next hop. If so, then we'll cancel the HTLC linkErr := l.validateHtlcAmount(
// directly. policy, payHash, amt, originalScid, customRecords,
if amt < policy.MinHTLCOut { )
l.log.Warnf("outgoing htlc(%x) is too small: min_htlc=%v, "+ if linkErr != nil {
"htlc_value=%v", payHash[:], policy.MinHTLCOut, return linkErr
amt)
// As part of the returned error, we'll send our latest routing
// policy so the sending node obtains the most up to date data.
cb := func(upd *lnwire.ChannelUpdate1) lnwire.FailureMessage {
return lnwire.NewAmountBelowMinimum(amt, *upd)
}
failure := l.createFailureWithUpdate(false, originalScid, cb)
return NewLinkError(failure)
}
// Next, ensure that the passed HTLC isn't too large. If so, we'll
// cancel the HTLC directly.
if policy.MaxHTLC != 0 && amt > policy.MaxHTLC {
l.log.Warnf("outgoing htlc(%x) is too large: max_htlc=%v, "+
"htlc_value=%v", payHash[:], policy.MaxHTLC, amt)
// As part of the returned error, we'll send our latest routing
// policy so the sending node obtains the most up-to-date data.
cb := func(upd *lnwire.ChannelUpdate1) lnwire.FailureMessage {
return lnwire.NewTemporaryChannelFailure(upd)
}
failure := l.createFailureWithUpdate(false, originalScid, cb)
return NewDetailedLinkError(failure, OutgoingFailureHTLCExceedsMax)
} }
// We want to avoid offering an HTLC which will expire in the near // We want to avoid offering an HTLC which will expire in the near
@@ -2591,6 +2567,7 @@ func (l *channelLink) canSendHtlc(policy models.ForwardingPolicy,
return lnwire.NewExpiryTooSoon(*upd) return lnwire.NewExpiryTooSoon(*upd)
} }
failure := l.createFailureWithUpdate(false, originalScid, cb) failure := l.createFailureWithUpdate(false, originalScid, cb)
return NewLinkError(failure) return NewLinkError(failure)
} }
@@ -2606,7 +2583,8 @@ func (l *channelLink) canSendHtlc(policy models.ForwardingPolicy,
// We now check the available bandwidth to see if this HTLC can be // We now check the available bandwidth to see if this HTLC can be
// forwarded. // forwarded.
availableBandwidth := l.Bandwidth() availableBandwidth := l.Bandwidth()
auxBandwidth, err := fn.MapOptionZ(
auxBandwidth, externalErr := fn.MapOptionZ(
l.cfg.AuxTrafficShaper, l.cfg.AuxTrafficShaper,
func(ts AuxTrafficShaper) fn.Result[OptionalBandwidth] { func(ts AuxTrafficShaper) fn.Result[OptionalBandwidth] {
var htlcBlob fn.Option[tlv.Blob] var htlcBlob fn.Option[tlv.Blob]
@@ -2624,8 +2602,10 @@ func (l *channelLink) canSendHtlc(policy models.ForwardingPolicy,
return l.AuxBandwidth(amt, originalScid, htlcBlob, ts) return l.AuxBandwidth(amt, originalScid, htlcBlob, ts)
}, },
).Unpack() ).Unpack()
if err != nil { if externalErr != nil {
l.log.Errorf("Unable to determine aux bandwidth: %v", err) l.log.Errorf("Unable to determine aux bandwidth: %v",
externalErr)
return NewLinkError(&lnwire.FailTemporaryNodeFailure{}) return NewLinkError(&lnwire.FailTemporaryNodeFailure{})
} }
@@ -2645,6 +2625,7 @@ func (l *channelLink) canSendHtlc(policy models.ForwardingPolicy,
return lnwire.NewTemporaryChannelFailure(upd) return lnwire.NewTemporaryChannelFailure(upd)
} }
failure := l.createFailureWithUpdate(false, originalScid, cb) failure := l.createFailureWithUpdate(false, originalScid, cb)
return NewDetailedLinkError( return NewDetailedLinkError(
failure, OutgoingFailureInsufficientBalance, failure, OutgoingFailureInsufficientBalance,
) )
@@ -4716,3 +4697,71 @@ func (l *channelLink) processLocalUpdateFailHTLC(ctx context.Context,
// Immediately update the commitment tx to minimize latency. // Immediately update the commitment tx to minimize latency.
l.updateCommitTxOrFail(ctx) l.updateCommitTxOrFail(ctx)
} }
// validateHtlcAmount checks if the HTLC amount is within the policy's
// minimum and maximum limits. Returns a LinkError if validation fails.
func (l *channelLink) validateHtlcAmount(policy models.ForwardingPolicy,
payHash [32]byte, amt lnwire.MilliSatoshi,
originalScid lnwire.ShortChannelID,
customRecords lnwire.CustomRecords) *LinkError {
// In case we are dealing with a custom HTLC, we don't need to validate
// the HTLC constraints.
//
// NOTE: Custom HTLCs are only locally sourced and will use custom
// channels which are not routable channels and should have their policy
// not restricted in the first place. However to be sure we skip this
// check otherwise we might end up in a loop of sending to the same
// route again and again because link errors are not persisted in
// mission control.
if fn.MapOptionZ(
l.cfg.AuxTrafficShaper,
func(ts AuxTrafficShaper) bool {
return ts.IsCustomHTLC(customRecords)
},
) {
l.log.Debugf("Skipping htlc amount policy validation for " +
"custom htlc")
return nil
}
// As our first sanity check, we'll ensure that the passed HTLC isn't
// too small for the next hop. If so, then we'll cancel the HTLC
// directly.
if amt < policy.MinHTLCOut {
l.log.Warnf("outgoing htlc(%x) is too small: min_htlc=%v, "+
"htlc_value=%v", payHash[:], policy.MinHTLCOut,
amt)
// As part of the returned error, we'll send our latest routing
// policy so the sending node obtains the most up to date data.
cb := func(upd *lnwire.ChannelUpdate1) lnwire.FailureMessage {
return lnwire.NewAmountBelowMinimum(amt, *upd)
}
failure := l.createFailureWithUpdate(false, originalScid, cb)
return NewLinkError(failure)
}
// Next, ensure that the passed HTLC isn't too large. If so, we'll
// cancel the HTLC directly.
if policy.MaxHTLC != 0 && amt > policy.MaxHTLC {
l.log.Warnf("outgoing htlc(%x) is too large: max_htlc=%v, "+
"htlc_value=%v", payHash[:], policy.MaxHTLC, amt)
// As part of the returned error, we'll send our latest routing
// policy so the sending node obtains the most up-to-date data.
cb := func(upd *lnwire.ChannelUpdate1) lnwire.FailureMessage {
return lnwire.NewTemporaryChannelFailure(upd)
}
failure := l.createFailureWithUpdate(false, originalScid, cb)
return NewDetailedLinkError(
failure, OutgoingFailureHTLCExceedsMax,
)
}
return nil
}

View File

@@ -24,9 +24,9 @@ type bandwidthHints interface {
availableChanBandwidth(channelID uint64, availableChanBandwidth(channelID uint64,
amount lnwire.MilliSatoshi) (lnwire.MilliSatoshi, bool) amount lnwire.MilliSatoshi) (lnwire.MilliSatoshi, bool)
// firstHopCustomBlob returns the custom blob for the first hop of the // isCustomHTLCPayment returns true if this payment is a custom payment.
// payment, if available. // For custom payments policy checks might not be needed.
firstHopCustomBlob() fn.Option[tlv.Blob] isCustomHTLCPayment() bool
} }
// getLinkQuery is the function signature used to lookup a link. // getLinkQuery is the function signature used to lookup a link.
@@ -207,8 +207,23 @@ func (b *bandwidthManager) availableChanBandwidth(channelID uint64,
return bandwidth, true return bandwidth, true
} }
// firstHopCustomBlob returns the custom blob for the first hop of the payment, // isCustomHTLCPayment returns true if this payment is a custom payment.
// if available. // For custom payments policy checks might not be needed.
func (b *bandwidthManager) firstHopCustomBlob() fn.Option[tlv.Blob] { func (b *bandwidthManager) isCustomHTLCPayment() bool {
return b.firstHopBlob return fn.MapOptionZ(b.firstHopBlob, func(blob tlv.Blob) bool {
customRecords, err := lnwire.ParseCustomRecords(blob)
if err != nil {
log.Warnf("failed to parse custom records when "+
"checking if payment is custom: %v", err)
return false
}
return fn.MapOptionZ(
b.trafficShaper,
func(s htlcswitch.AuxTrafficShaper) bool {
return s.IsCustomHTLC(customRecords)
},
)
})
} }

View File

@@ -11,7 +11,6 @@ import (
"github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/tlv"
"github.com/lightningnetwork/lnd/zpay32" "github.com/lightningnetwork/lnd/zpay32"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -36,8 +35,8 @@ func (m *mockBandwidthHints) availableChanBandwidth(channelID uint64,
return balance, ok return balance, ok
} }
func (m *mockBandwidthHints) firstHopCustomBlob() fn.Option[tlv.Blob] { func (m *mockBandwidthHints) isCustomHTLCPayment() bool {
return fn.None[tlv.Blob]() return false
} }
// integratedRoutingContext defines the context in which integrated routing // integratedRoutingContext defines the context in which integrated routing

View File

@@ -265,12 +265,13 @@ func (u *edgeUnifier) getEdgeLocal(netAmtReceived lnwire.MilliSatoshi,
// Add inbound fee to get to the amount that is sent over the // Add inbound fee to get to the amount that is sent over the
// local channel. // local channel.
amt := netAmtReceived + lnwire.MilliSatoshi(inboundFee) amt := netAmtReceived + lnwire.MilliSatoshi(inboundFee)
// Check valid amount range for the channel. We skip this test // Check valid amount range for the channel. We skip this test
// for payments with custom HTLC data, as the amount sent on
// the BTC layer may differ from the amount that is actually // for payments with custom htlc data we skip the amount range
// forwarded in custom channels. // check because the amt of the payment does not relate to the
if bandwidthHints.firstHopCustomBlob().IsNone() && // actual amount carried by the HTLC but instead is encoded in
// the blob data.
if !bandwidthHints.isCustomHTLCPayment() &&
!edge.amtInRange(amt) { !edge.amtInRange(amt) {
log.Debugf("Amount %v not in range for edge %v", log.Debugf("Amount %v not in range for edge %v",