Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

routing: add TlvTrafficShaper to bandwidth hints #8665

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
"github.com/lightningnetwork/lnd/lnwallet/rpcwallet"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/rpcperms"
"github.com/lightningnetwork/lnd/signal"
"github.com/lightningnetwork/lnd/sqldb"
Expand Down Expand Up @@ -157,6 +158,10 @@ type AuxComponents struct {
// AuxLeafStore is an optional data source that can be used by custom
// channels to fetch+store various data.
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]

// TrafficShaper is an optional traffic shaper that can be used to
// control the outgoing channel of a payment.
TrafficShaper fn.Option[routing.TlvTrafficShaper]
}

// DefaultWalletImpl is the default implementation of our normal, btcwallet
Expand Down
11 changes: 11 additions & 0 deletions htlcswitch/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/tlv"
)

// InvoiceDatabase is an interface which represents the persistent subsystem
Expand Down Expand Up @@ -271,6 +272,16 @@ type ChannelLink interface {
// have buffered messages.
AttachMailBox(MailBox)

// FundingCustomBlob returns the custom funding blob of the channel that
// this link is associated with. The funding blob represents static
// information about the channel that was created at channel funding
// time.
FundingCustomBlob() fn.Option[tlv.Blob]

// CommitmentCustomBlob returns the custom blob of the current local
// commitment of the channel that this link is associated with.
CommitmentCustomBlob() fn.Option[tlv.Blob]

// Start/Stop are used to initiate the start/stop of the channel link
// functioning.
Start() error
Expand Down
13 changes: 13 additions & 0 deletions htlcswitch/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -3775,3 +3775,16 @@ func (l *channelLink) fail(linkErr LinkFailureError,
l.failed = true
l.cfg.OnChannelFailure(l.ChanID(), l.ShortChanID(), linkErr)
}

// FundingCustomBlob returns the custom funding blob of the channel that this
// link is associated with. The funding blob represents static information about
// the channel that was created at channel funding time.
func (l *channelLink) FundingCustomBlob() fn.Option[tlv.Blob] {
return l.channel.State().CustomBlob
}

// CommitmentCustomBlob returns the custom blob of the current local commitment
// of the channel that this link is associated with.
func (l *channelLink) CommitmentCustomBlob() fn.Option[tlv.Blob] {
return l.channel.LocalCommitmentBlob()
}
14 changes: 14 additions & 0 deletions htlcswitch/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/contractcourt"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/lnpeer"
Expand All @@ -35,6 +36,7 @@ import (
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/ticker"
"github.com/lightningnetwork/lnd/tlv"
)

func isAlias(scid lnwire.ShortChannelID) bool {
Expand Down Expand Up @@ -912,6 +914,10 @@ func (f *mockChannelLink) ChannelPoint() wire.OutPoint {
return wire.OutPoint{}
}

func (f *mockChannelLink) ChannelCustomBlob() fn.Option[tlv.Blob] {
return fn.Option[tlv.Blob]{}
}

func (f *mockChannelLink) Stop() {}
func (f *mockChannelLink) EligibleToForward() bool { return f.eligible }
func (f *mockChannelLink) MayAddOutgoingHtlc(lnwire.MilliSatoshi) error { return nil }
Expand Down Expand Up @@ -942,6 +948,14 @@ func (f *mockChannelLink) OnCommitOnce(LinkDirection, func()) {
// TODO(proofofkeags): Implement
}

func (f *mockChannelLink) FundingCustomBlob() fn.Option[tlv.Blob] {
return fn.None[tlv.Blob]()
}

func (f *mockChannelLink) CommitmentCustomBlob() fn.Option[tlv.Blob] {
return fn.None[tlv.Blob]()
}

var _ ChannelLink = (*mockChannelLink)(nil)

func newDB() (*channeldb.DB, func(), error) {
Expand Down
16 changes: 16 additions & 0 deletions lnwallet/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -9589,3 +9589,19 @@ func (lc *LightningChannel) MultiSigKeys() (keychain.KeyDescriptor,
return lc.channelState.LocalChanCfg.MultiSigKey,
lc.channelState.RemoteChanCfg.MultiSigKey
}

// LocalCommitmentBlob returns the custom blob of the local commitment.
func (lc *LightningChannel) LocalCommitmentBlob() fn.Option[tlv.Blob] {
lc.RLock()
defer lc.RUnlock()

chanState := lc.channelState
localBalance := chanState.LocalCommitment.CustomBlob

return fn.MapOption(func(b tlv.Blob) tlv.Blob {
newBlob := make([]byte, len(b))
copy(newBlob, b)

return newBlob
})(localBalance)
}
124 changes: 109 additions & 15 deletions routing/bandwidth.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package routing

import (
"fmt"

"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
Expand All @@ -18,7 +23,39 @@ 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 {
AuxHtlcModifier

// 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,
fundingBlob fn.Option[tlv.Blob]) (bool, error)

// PaymentBandwidth returns the available bandwidth for a custom channel
// decided by the given channel aux blob and HTLC blob. A return value
// of 0 means there is no bandwidth available. To find out if a channel
// is a custom channel that should be handled by the traffic shaper, the
// HandleTraffic method should be called first.
PaymentBandwidth(htlcBlob,
commitmentBlob fn.Option[tlv.Blob]) (lnwire.MilliSatoshi, error)
}

// 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(totalAmount lnwire.MilliSatoshi,
htlcBlob tlv.Blob) (btcutil.Amount, tlv.Blob, error)
}

// getLinkQuery is the function signature used to lookup a link.
Expand All @@ -29,8 +66,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
Expand All @@ -40,11 +78,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
Expand All @@ -71,7 +111,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 {
Expand All @@ -89,30 +130,83 @@ 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.
var (
auxBandwidth lnwire.MilliSatoshi
auxBandwidthDetermined bool
)
err = fn.MapOptionZ(b.trafficShaper, func(ts TlvTrafficShaper) error {
fundingBlob := link.FundingCustomBlob()
shouldHandle, err := ts.HandleTraffic(cid, fundingBlob)
if err != nil {
return fmt.Errorf("traffic shaper failed to decide "+
"whether to handle traffic: %w", err)
}

log.Debugf("ShortChannelID=%v: external traffic shaper is "+
"handling traffic: %v", cid, shouldHandle)

// If this channel isn't handled by the external traffic shaper,
// we'll return early.
if !shouldHandle {
return nil
}

// Ask for a specific bandwidth to be used for the channel.
commitmentBlob := link.CommitmentCustomBlob()
auxBandwidth, err = ts.PaymentBandwidth(
htlcBlob, commitmentBlob,
)
if err != nil {
return fmt.Errorf("failed to get bandwidth from "+
"external traffic shaper: %w", err)
}

log.Debugf("ShortChannelID=%v: external traffic shaper "+
"reported available bandwidth: %v", cid, auxBandwidth)

auxBandwidthDetermined = true

return nil
})
if err != nil {
log.Errorf("ShortChannelID=%v: failed to get bandwidth from "+
"external traffic shaper: %v", cid, err)

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)
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.
guggero marked this conversation as resolved.
Show resolved Hide resolved
// If the external traffic shaper determined the bandwidth, we'll return
// that value, even if it is zero (which would mean no bandwidth is
// available on that channel).
if auxBandwidthDetermined {
return auxBandwidth
}

// Otherwise, we'll return the current best estimate for the
// available bandwidth for the link.
return link.Bandwidth()
}

// availableChanBandwidth returns the total available bandwidth for a channel
// 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]
if !ok {
return 0, false
}

return b.getBandwidth(shortID, amount), true
return b.getBandwidth(shortID, amount, htlcBlob), true
}
3 changes: 3 additions & 0 deletions routing/bandwidth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/btcsuite/btcd/btcutil"
"github.com/go-errors/errors"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -115,11 +116,13 @@ func TestBandwidthManager(t *testing.T) {

m, err := newBandwidthManager(
g, sourceNode.pubkey, testCase.linkQuery,
fn.None[TlvTrafficShaper](),
)
require.NoError(t, err)

bandwidth, found := m.availableChanBandwidth(
testCase.channelID, 10,
fn.None[[]byte](),
)
require.Equal(t, testCase.expectedBandwidth, bandwidth)
require.Equal(t, testCase.expectFound, found)
Expand Down
5 changes: 4 additions & 1 deletion routing/integrated_routing_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"
"time"

"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
Expand All @@ -24,7 +25,8 @@ type mockBandwidthHints struct {
}

func (m *mockBandwidthHints) availableChanBandwidth(channelID uint64,
_ lnwire.MilliSatoshi) (lnwire.MilliSatoshi, bool) {
_ lnwire.MilliSatoshi,
htlcBlob fn.Option[[]byte]) (lnwire.MilliSatoshi, bool) {

if m.hints == nil {
return 0, false
Expand Down Expand Up @@ -229,6 +231,7 @@ func (c *integratedRoutingContext) testPayment(maxParts uint32,
// Find a route.
route, err := session.RequestRoute(
amtRemaining, lnwire.MaxMilliSatoshi, inFlightHtlcs, 0,
nil,
)
if err != nil {
return attempts, err
Expand Down
Loading
Loading