From 57e975be6d373d086c81bc0be8b5f98581070769 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Fri, 19 Apr 2024 13:49:45 +0200 Subject: [PATCH] routing: add TlvTrafficShaper to bandwidth hints --- routing/bandwidth.go | 116 +++++++++++++++++++++++++----- routing/payment_session_source.go | 7 +- routing/router.go | 19 +++-- routing/unified_edges.go | 12 ++-- 4 files changed, 129 insertions(+), 25 deletions(-) diff --git a/routing/bandwidth.go b/routing/bandwidth.go index 19c608701..7d33d30c7 100644 --- a/routing/bandwidth.go +++ b/routing/bandwidth.go @@ -1,10 +1,13 @@ package routing import ( + "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/tlv" ) // bandwidthHints provides hints about the currently available balance in our @@ -18,7 +21,36 @@ type bandwidthHints interface { // will be used. If the channel is unavailable, a zero amount is // returned. availableChanBandwidth(channelID uint64, - amount lnwire.MilliSatoshi) (lnwire.MilliSatoshi, bool) + amount lnwire.MilliSatoshi, + htlcBlob fn.Option[tlv.Blob]) (lnwire.MilliSatoshi, bool) +} + +// TlvTrafficShaper is an interface that allows the sender to determine if a +// payment should be carried by a channel based on the TLV records that may be +// present in the `update_add_htlc` message or the channel commitment itself. +type TlvTrafficShaper interface { + // ShouldCarryPayment returns true if the provided tlv records indicate + // that this channel may carry out the payment by utilizing external + // mechanisms. + ShouldCarryPayment(amt lnwire.MilliSatoshi, htlcTLV, + channelBlob fn.Option[tlv.Blob]) bool + + // HandleTraffic is called in order to check if the channel identified + // by the provided channel ID may have external mechanisms that would + // allow it to carry out the payment. + HandleTraffic(cid lnwire.ShortChannelID) bool + + AuxHtlcModifier +} + +// AuxHtlcModifier is an interface that allows the sender to modify the outgoing +// HTLC of a payment by changing the amount or the wire message tlv records. +type AuxHtlcModifier interface { + // ProduceHtlcExtraData is a function that, based on the previous extra + // data blob of an htlc, may produce a different blob or modify the + // amount of bitcoin this htlc should carry. + ProduceHtlcExtraData(htlcBlob tlv.Blob, + chanID uint64) (btcutil.Amount, tlv.Blob, error) } // getLinkQuery is the function signature used to lookup a link. @@ -29,8 +61,9 @@ type getLinkQuery func(lnwire.ShortChannelID) ( // uses the link lookup provided to query the link for our latest local channel // balances. type bandwidthManager struct { - getLink getLinkQuery - localChans map[lnwire.ShortChannelID]struct{} + getLink getLinkQuery + localChans map[lnwire.ShortChannelID]struct{} + trafficShaper fn.Option[TlvTrafficShaper] } // newBandwidthManager creates a bandwidth manager for the source node provided @@ -40,11 +73,13 @@ type bandwidthManager struct { // allows us to reduce the number of extraneous attempts as we can skip channels // that are inactive, or just don't have enough bandwidth to carry the payment. func newBandwidthManager(graph routingGraph, sourceNode route.Vertex, - linkQuery getLinkQuery) (*bandwidthManager, error) { + linkQuery getLinkQuery, + trafficShaper fn.Option[TlvTrafficShaper]) (*bandwidthManager, error) { manager := &bandwidthManager{ - getLink: linkQuery, - localChans: make(map[lnwire.ShortChannelID]struct{}), + getLink: linkQuery, + localChans: make(map[lnwire.ShortChannelID]struct{}), + trafficShaper: trafficShaper, } // First, we'll collect the set of outbound edges from the target @@ -71,7 +106,8 @@ func newBandwidthManager(graph routingGraph, sourceNode route.Vertex, // queried is one of our local channels, so any failure to retrieve the link // is interpreted as the link being offline. func (b *bandwidthManager) getBandwidth(cid lnwire.ShortChannelID, - amount lnwire.MilliSatoshi) lnwire.MilliSatoshi { + amount lnwire.MilliSatoshi, + htlcBlob fn.Option[tlv.Blob]) lnwire.MilliSatoshi { link, err := b.getLink(cid) if err != nil { @@ -89,16 +125,63 @@ func (b *bandwidthManager) getBandwidth(cid lnwire.ShortChannelID, return 0 } - // If our link isn't currently in a state where it can add another - // outgoing htlc, treat the link as unusable. - if err := link.MayAddOutgoingHtlc(amount); err != nil { - log.Warnf("ShortChannelID=%v: cannot add outgoing htlc: %v", - cid, err) + res := fn.MapOptionZ(b.trafficShaper, func(ts TlvTrafficShaper) bool { + return ts.HandleTraffic(cid) + }) + + // If response is no and we do have a traffic shaper, we can't handle + // the traffic and return early. + if !res && b.trafficShaper.IsSome() { + log.Warnf("ShortChannelID=%v: can't handle traffic", cid) return 0 } - // Otherwise, we'll return the current best estimate for the available - // bandwidth for the link. + channelBlob := link.ChannelCustomBlob() + + // Run the wrapped traffic if it exists, otherwise return false. + res = fn.MapOptionZ(b.trafficShaper, func(ts TlvTrafficShaper) bool { + return ts.ShouldCarryPayment(amount, htlcBlob, channelBlob) + }) + + // If the traffic shaper indicates that this channel can route the + // payment, we immediatelly select this channel and return maximum + // bandwidth as response. + if res { + // If the amount is zero, but the traffic shaper signaled that + // the channel can carry the payment, we'll return the maximum + // amount. A zero amount is used when we try to figure out if + // enough balance exists for the payment to be carried out, but + // at that point we don't know the payment amount in order to + // return an exact value, so we signal a value that will + // certainly satisfy the payment amount. + if amount == 0 { + // We don't want to just return the max uint64 as this + // will overflow when further amounts are added + // together. + return lnwire.MaxMilliSatoshi / 2 + } + + return amount + } + + // If the traffic shaper is present and it returned false, we want to + // skip this channel. + if b.trafficShaper.IsSome() { + log.Warnf("ShortChannelID=%v: payment not allowed by traffic "+ + "shaper", cid) + return 0 + } + + // If our link isn't currently in a state where it can add + // another outgoing htlc, treat the link as unusable. + if err := link.MayAddOutgoingHtlc(amount); err != nil { + log.Warnf("ShortChannelID=%v: cannot add outgoing "+ + "htlc: %v", cid, err) + return 0 + } + + // Otherwise, we'll return the current best estimate for the + // available bandwidth for the link. return link.Bandwidth() } @@ -106,7 +189,8 @@ func (b *bandwidthManager) getBandwidth(cid lnwire.ShortChannelID, // and a bool indicating whether the channel hint was found. If the channel is // unavailable, a zero amount is returned. func (b *bandwidthManager) availableChanBandwidth(channelID uint64, - amount lnwire.MilliSatoshi) (lnwire.MilliSatoshi, bool) { + amount lnwire.MilliSatoshi, + htlcBlob fn.Option[tlv.Blob]) (lnwire.MilliSatoshi, bool) { shortID := lnwire.NewShortChanIDFromInt(channelID) _, ok := b.localChans[shortID] @@ -114,5 +198,5 @@ func (b *bandwidthManager) availableChanBandwidth(channelID uint64, return 0, false } - return b.getBandwidth(shortID, amount), true + return b.getBandwidth(shortID, amount, htlcBlob), true } diff --git a/routing/payment_session_source.go b/routing/payment_session_source.go index b96a2294b..968a098cb 100644 --- a/routing/payment_session_source.go +++ b/routing/payment_session_source.go @@ -4,6 +4,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/zpay32" @@ -42,6 +43,8 @@ type SessionSource struct { // PathFindingConfig defines global parameters that control the // trade-off in path finding between fees and probability. PathFindingConfig PathFindingConfig + + TrafficShaper fn.Option[TlvTrafficShaper] } // getRoutingGraph returns a routing graph and a clean-up function for @@ -63,12 +66,14 @@ func (m *SessionSource) getRoutingGraph() (routingGraph, func(), error) { // view from Mission Control. An optional set of routing hints can be provided // in order to populate additional edges to explore when finding a path to the // payment's destination. -func (m *SessionSource) NewPaymentSession(p *LightningPayment) ( +func (m *SessionSource) NewPaymentSession(p *LightningPayment, + trafficShaper fn.Option[TlvTrafficShaper]) ( PaymentSession, error) { getBandwidthHints := func(graph routingGraph) (bandwidthHints, error) { return newBandwidthManager( graph, m.SourceNode.PubKeyBytes, m.GetLink, + trafficShaper, ) } diff --git a/routing/router.go b/routing/router.go index 8a3275779..4e1fe2154 100644 --- a/routing/router.go +++ b/routing/router.go @@ -245,7 +245,8 @@ type PaymentSessionSource interface { // routes to the given target. An optional set of routing hints can be // provided in order to populate additional edges to explore when // finding a path to the payment's destination. - NewPaymentSession(p *LightningPayment) (PaymentSession, error) + NewPaymentSession(p *LightningPayment, + trafficShaper fn.Option[TlvTrafficShaper]) (PaymentSession, error) // NewPaymentSessionEmpty creates a new paymentSession instance that is // empty, and will be exhausted immediately. Used for failure reporting @@ -411,6 +412,8 @@ type Config struct { // IsAlias returns whether a passed ShortChannelID is an alias. This is // only used for our local channels. IsAlias func(scid lnwire.ShortChannelID) bool + + TrafficShaper fn.Option[TlvTrafficShaper] } // EdgeLocator is a struct used to identify a specific edge. @@ -2104,6 +2107,7 @@ func (r *ChannelRouter) FindRoute(req *RouteRequest) (*route.Route, float64, // eliminate certain routes early on in the path finding process. bandwidthHints, err := newBandwidthManager( r.cachedGraph, r.selfNode.PubKeyBytes, r.cfg.GetLink, + r.cfg.TrafficShaper, ) if err != nil { return nil, 0, err @@ -2466,7 +2470,9 @@ func (r *ChannelRouter) PreparePayment(payment *LightningPayment) ( // Before starting the HTLC routing attempt, we'll create a fresh // payment session which will report our errors back to mission // control. - paySession, err := r.cfg.SessionSource.NewPaymentSession(payment) + paySession, err := r.cfg.SessionSource.NewPaymentSession( + payment, r.cfg.TrafficShaper, + ) if err != nil { return nil, nil, err } @@ -3115,6 +3121,7 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi, // the best outgoing channel to use in case no outgoing channel is set. bandwidthHints, err := newBandwidthManager( r.cachedGraph, r.selfNode.PubKeyBytes, r.cfg.GetLink, + r.cfg.TrafficShaper, ) if err != nil { return nil, err @@ -3211,7 +3218,9 @@ func getRouteUnifiers(source route.Vertex, hops []route.Vertex, } // Get an edge for the specific amount that we want to forward. - edge := edgeUnifier.getEdge(runningAmt, bandwidthHints, 0) + edge := edgeUnifier.getEdge( + runningAmt, bandwidthHints, 0, fn.Option[[]byte]{}, + ) if edge == nil { log.Errorf("Cannot find policy with amt=%v for node %v", runningAmt, fromNode) @@ -3249,7 +3258,9 @@ func getPathEdges(source route.Vertex, receiverAmt lnwire.MilliSatoshi, // amount ranges re-checked. var pathEdges []*unifiedEdge for i, unifier := range unifiers { - edge := unifier.getEdge(receiverAmt, bandwidthHints, 0) + edge := unifier.getEdge( + receiverAmt, bandwidthHints, 0, fn.Option[[]byte]{}, + ) if edge == nil { fromNode := source if i > 0 { diff --git a/routing/unified_edges.go b/routing/unified_edges.go index 44efc6314..a02bddf3e 100644 --- a/routing/unified_edges.go +++ b/routing/unified_edges.go @@ -6,9 +6,11 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/tlv" ) // nodeEdgeUnifier holds all edge unifiers for connections towards a node. @@ -182,11 +184,12 @@ type edgeUnifier struct { // channels. func (u *edgeUnifier) getEdge(netAmtReceived lnwire.MilliSatoshi, bandwidthHints bandwidthHints, - nextOutFee lnwire.MilliSatoshi) *unifiedEdge { + nextOutFee lnwire.MilliSatoshi, + htlcBlob fn.Option[tlv.Blob]) *unifiedEdge { if u.localChan { return u.getEdgeLocal( - netAmtReceived, bandwidthHints, nextOutFee, + netAmtReceived, bandwidthHints, nextOutFee, htlcBlob, ) } @@ -214,7 +217,8 @@ func calcCappedInboundFee(edge *unifiedEdge, amt lnwire.MilliSatoshi, // connection given a specific amount to send. func (u *edgeUnifier) getEdgeLocal(netAmtReceived lnwire.MilliSatoshi, bandwidthHints bandwidthHints, - nextOutFee lnwire.MilliSatoshi) *unifiedEdge { + nextOutFee lnwire.MilliSatoshi, + htlcBlob fn.Option[tlv.Blob]) *unifiedEdge { var ( bestEdge *unifiedEdge @@ -251,7 +255,7 @@ func (u *edgeUnifier) getEdgeLocal(netAmtReceived lnwire.MilliSatoshi, // channel. The bandwidth hint is expected to be // available. bandwidth, ok := bandwidthHints.availableChanBandwidth( - edge.policy.ChannelID, amt, + edge.policy.ChannelID, amt, htlcBlob, ) if !ok { log.Debugf("Cannot get bandwidth for edge %v, use max "+