routing: add TlvTrafficShaper to bandwidth hints

This commit is contained in:
George Tsagkarelis 2024-04-19 13:49:45 +02:00 committed by Oliver Gugger
parent 5dcda25881
commit 5b4de5f0d4
No known key found for this signature in database
GPG key ID: 8E4256593F177720
12 changed files with 314 additions and 43 deletions

View file

@ -1,10 +1,14 @@
package routing
import (
"fmt"
"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
@ -19,6 +23,42 @@ type bandwidthHints interface {
// returned.
availableChanBandwidth(channelID uint64,
amount lnwire.MilliSatoshi) (lnwire.MilliSatoshi, bool)
// firstHopCustomBlob returns the custom blob for the first hop of the
// payment, if available.
firstHopCustomBlob() fn.Option[tlv.Blob]
}
// 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
// ShouldHandleTraffic 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.
ShouldHandleTraffic(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],
linkBandwidth lnwire.MilliSatoshi) (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,
htlcCustomRecords lnwire.CustomRecords) (lnwire.MilliSatoshi,
lnwire.CustomRecords, error)
}
// getLinkQuery is the function signature used to lookup a link.
@ -31,6 +71,8 @@ type getLinkQuery func(lnwire.ShortChannelID) (
type bandwidthManager struct {
getLink getLinkQuery
localChans map[lnwire.ShortChannelID]struct{}
firstHopBlob fn.Option[tlv.Blob]
trafficShaper fn.Option[TlvTrafficShaper]
}
// newBandwidthManager creates a bandwidth manager for the source node provided
@ -40,11 +82,14 @@ 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 Graph, sourceNode route.Vertex,
linkQuery getLinkQuery) (*bandwidthManager, error) {
linkQuery getLinkQuery, firstHopBlob fn.Option[tlv.Blob],
trafficShaper fn.Option[TlvTrafficShaper]) (*bandwidthManager, error) {
manager := &bandwidthManager{
getLink: linkQuery,
localChans: make(map[lnwire.ShortChannelID]struct{}),
firstHopBlob: firstHopBlob,
trafficShaper: trafficShaper,
}
// First, we'll collect the set of outbound edges from the target
@ -89,17 +134,111 @@ 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)
// bandwidthResult is an inline type that we'll use to pass the
// bandwidth result from the external traffic shaper to the main logic
// below.
type bandwidthResult struct {
// bandwidth is the available bandwidth for the channel as
// reported by the external traffic shaper. If the external
// traffic shaper is not handling the channel, this value will
// be fn.None
bandwidth fn.Option[lnwire.MilliSatoshi]
// htlcAmount is the amount we're going to use to check if we
// can add another HTLC to the channel. If the external traffic
// shaper is handling the channel, we'll use 0 to just sanity
// check the number of HTLCs on the channel, since we don't know
// the actual HTLC amount that will be sent.
htlcAmount fn.Option[lnwire.MilliSatoshi]
}
var (
// We will pass the link bandwidth to the external traffic
// shaper. This is the current best estimate for the available
// bandwidth for the link.
linkBandwidth = link.Bandwidth()
bandwidthErr = func(err error) fn.Result[bandwidthResult] {
return fn.Err[bandwidthResult](err)
}
)
result, err := fn.MapOptionZ(
b.trafficShaper,
func(ts TlvTrafficShaper) fn.Result[bandwidthResult] {
fundingBlob := link.FundingCustomBlob()
shouldHandle, err := ts.ShouldHandleTraffic(
cid, fundingBlob,
)
if err != nil {
return bandwidthErr(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 fn.Ok(bandwidthResult{})
}
// Ask for a specific bandwidth to be used for the
// channel.
commitmentBlob := link.CommitmentCustomBlob()
auxBandwidth, err := ts.PaymentBandwidth(
b.firstHopBlob, commitmentBlob, linkBandwidth,
)
if err != nil {
return bandwidthErr(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)
// We don't know the actual HTLC amount that will be
// sent using the custom channel. But we'll still want
// to make sure we can add another HTLC, using the
// MayAddOutgoingHtlc method below. Passing 0 into that
// method will use the minimum HTLC value for the
// channel, which is okay to just check we don't exceed
// the max number of HTLCs on the channel. A proper
// balance check is done elsewhere.
return fn.Ok(bandwidthResult{
bandwidth: fn.Some(auxBandwidth),
htlcAmount: fn.Some[lnwire.MilliSatoshi](0),
})
},
).Unpack()
if err != nil {
log.Errorf("ShortChannelID=%v: failed to get bandwidth from "+
"external traffic shaper: %v", cid, err)
return 0
}
// Otherwise, we'll return the current best estimate for the available
// bandwidth for the link.
return link.Bandwidth()
htlcAmount := result.htlcAmount.UnwrapOr(amount)
// 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(htlcAmount); err != nil {
log.Warnf("ShortChannelID=%v: cannot add outgoing "+
"htlc with amount %v: %v", cid, htlcAmount, err)
return 0
}
// 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).
reportedBandwidth := result.bandwidth.UnwrapOr(linkBandwidth)
return reportedBandwidth
}
// availableChanBandwidth returns the total available bandwidth for a channel
@ -116,3 +255,9 @@ func (b *bandwidthManager) availableChanBandwidth(channelID uint64,
return b.getBandwidth(shortID, amount), true
}
// firstHopCustomBlob returns the custom blob for the first hop of the payment,
// if available.
func (b *bandwidthManager) firstHopCustomBlob() fn.Option[tlv.Blob] {
return b.firstHopBlob
}

View file

@ -5,8 +5,10 @@ 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/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
)
@ -115,6 +117,8 @@ func TestBandwidthManager(t *testing.T) {
m, err := newBandwidthManager(
g, sourceNode.pubkey, testCase.linkQuery,
fn.None[[]byte](),
fn.Some[TlvTrafficShaper](&mockTrafficShaper{}),
)
require.NoError(t, err)
@ -126,3 +130,35 @@ func TestBandwidthManager(t *testing.T) {
})
}
}
type mockTrafficShaper struct{}
// ShouldHandleTraffic 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.
func (*mockTrafficShaper) ShouldHandleTraffic(_ lnwire.ShortChannelID,
_ fn.Option[tlv.Blob]) (bool, error) {
return true, nil
}
// 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.
func (*mockTrafficShaper) PaymentBandwidth(_, _ fn.Option[tlv.Blob],
linkBandwidth lnwire.MilliSatoshi) (lnwire.MilliSatoshi, error) {
return linkBandwidth, nil
}
// 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.
func (*mockTrafficShaper) ProduceHtlcExtraData(totalAmount lnwire.MilliSatoshi,
_ lnwire.CustomRecords) (lnwire.MilliSatoshi, lnwire.CustomRecords,
error) {
return totalAmount, nil, nil
}

View file

@ -8,9 +8,11 @@ import (
"time"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/tlv"
"github.com/lightningnetwork/lnd/zpay32"
"github.com/stretchr/testify/require"
)
@ -35,6 +37,10 @@ func (m *mockBandwidthHints) availableChanBandwidth(channelID uint64,
return balance, ok
}
func (m *mockBandwidthHints) firstHopCustomBlob() fn.Option[tlv.Blob] {
return fn.None[tlv.Blob]()
}
// integratedRoutingContext defines the context in which integrated routing
// tests run.
type integratedRoutingContext struct {
@ -227,6 +233,9 @@ func (c *integratedRoutingContext) testPayment(maxParts uint32,
// Find a route.
route, err := session.RequestRoute(
amtRemaining, lnwire.MaxMilliSatoshi, inFlightHtlcs, 0,
lnwire.CustomRecords{
lnwire.MinCustomRecordsTlvType: []byte{1, 2, 3},
},
)
if err != nil {
return attempts, err

View file

@ -296,7 +296,7 @@ func testMppSend(t *testing.T, testCase *mppSendTestCase) {
case err == nil && testCase.expectedFailure:
t.Fatal("expected payment to fail")
case err != nil && !testCase.expectedFailure:
t.Fatal("expected payment to succeed")
t.Fatalf("expected payment to succeed, got %v", err)
}
if len(attempts) != testCase.expectedAttempts {

View file

@ -9,12 +9,14 @@ import (
"github.com/go-errors/errors"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/routing/shards"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/mock"
)
@ -104,7 +106,8 @@ type mockPaymentSessionSourceOld struct {
var _ PaymentSessionSource = (*mockPaymentSessionSourceOld)(nil)
func (m *mockPaymentSessionSourceOld) NewPaymentSession(
_ *LightningPayment) (PaymentSession, error) {
_ *LightningPayment, _ fn.Option[tlv.Blob],
_ fn.Option[TlvTrafficShaper]) (PaymentSession, error) {
return &mockPaymentSessionOld{
routes: m.routes,
@ -166,7 +169,8 @@ type mockPaymentSessionOld struct {
var _ PaymentSession = (*mockPaymentSessionOld)(nil)
func (m *mockPaymentSessionOld) RequestRoute(_, _ lnwire.MilliSatoshi,
_, height uint32) (*route.Route, error) {
_, height uint32, _ lnwire.CustomRecords) (*route.Route,
error) {
if m.release != nil {
m.release <- struct{}{}
@ -630,9 +634,10 @@ type mockPaymentSessionSource struct {
var _ PaymentSessionSource = (*mockPaymentSessionSource)(nil)
func (m *mockPaymentSessionSource) NewPaymentSession(
payment *LightningPayment) (PaymentSession, error) {
payment *LightningPayment, firstHopBlob fn.Option[tlv.Blob],
tlvShaper fn.Option[TlvTrafficShaper]) (PaymentSession, error) {
args := m.Called(payment)
args := m.Called(payment, firstHopBlob, tlvShaper)
return args.Get(0).(PaymentSession), args.Error(1)
}
@ -690,9 +695,12 @@ type mockPaymentSession struct {
var _ PaymentSession = (*mockPaymentSession)(nil)
func (m *mockPaymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
activeShards, height uint32) (*route.Route, error) {
activeShards, height uint32,
firstHopCustomRecords lnwire.CustomRecords) (*route.Route, error) {
args := m.Called(maxAmt, feeLimit, activeShards, height)
args := m.Called(
maxAmt, feeLimit, activeShards, height, firstHopCustomRecords,
)
// Type assertion on nil will fail, so we check and return here.
if args.Get(0) == nil {
@ -897,6 +905,14 @@ func (m *mockLink) MayAddOutgoingHtlc(_ lnwire.MilliSatoshi) error {
return m.mayAddOutgoingErr
}
func (m *mockLink) FundingCustomBlob() fn.Option[tlv.Blob] {
return fn.None[tlv.Blob]()
}
func (m *mockLink) CommitmentCustomBlob() fn.Option[tlv.Blob] {
return fn.None[tlv.Blob]()
}
type mockShardTracker struct {
mock.Mock
}

View file

@ -366,6 +366,7 @@ func (p *paymentLifecycle) requestRoute(
rt, err := p.paySession.RequestRoute(
ps.RemainingAmt, remainingFees,
uint32(ps.NumAttemptsInFlight), uint32(p.currentHeight),
p.firstHopCustomRecords,
)
// Exit early if there's no error.

View file

@ -9,6 +9,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/go-errors/errors"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/lnmock"
"github.com/lightningnetwork/lnd/lntest/wait"
@ -28,7 +29,11 @@ func createTestPaymentLifecycle() *paymentLifecycle {
paymentHash := lntypes.Hash{1, 2, 3}
quitChan := make(chan struct{})
rt := &ChannelRouter{
cfg: &Config{},
cfg: &Config{
TrafficShaper: fn.Some[TlvTrafficShaper](
&mockTrafficShaper{},
),
},
quit: quitChan,
}
@ -78,6 +83,9 @@ func newTestPaymentLifecycle(t *testing.T) (*paymentLifecycle, *mockers) {
Payer: mockPayer,
Clock: mockClock,
MissionControl: mockMissionControl,
TrafficShaper: fn.Some[TlvTrafficShaper](
&mockTrafficShaper{},
),
},
quit: quitChan,
}
@ -372,6 +380,7 @@ func TestRequestRouteSucceed(t *testing.T) {
// Mock the paySession's `RequestRoute` method to return no error.
paySession.On("RequestRoute",
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
mock.Anything,
).Return(dummyRoute, nil)
result, err := p.requestRoute(ps)
@ -408,6 +417,7 @@ func TestRequestRouteHandleCriticalErr(t *testing.T) {
// Mock the paySession's `RequestRoute` method to return an error.
paySession.On("RequestRoute",
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
mock.Anything,
).Return(nil, errDummy)
result, err := p.requestRoute(ps)
@ -442,6 +452,7 @@ func TestRequestRouteHandleNoRouteErr(t *testing.T) {
// type.
m.paySession.On("RequestRoute",
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
mock.Anything,
).Return(nil, errNoTlvPayload)
// The payment should be failed with reason no route.
@ -489,6 +500,7 @@ func TestRequestRouteFailPaymentError(t *testing.T) {
// Mock the paySession's `RequestRoute` method to return an error.
paySession.On("RequestRoute",
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
mock.Anything,
).Return(nil, errNoTlvPayload)
result, err := p.requestRoute(ps)
@ -865,7 +877,7 @@ func TestResumePaymentFailOnRequestRouteErr(t *testing.T) {
// 4. mock requestRoute to return an error.
m.paySession.On("RequestRoute",
paymentAmt, p.feeLimit, uint32(ps.NumAttemptsInFlight),
uint32(p.currentHeight),
uint32(p.currentHeight), mock.Anything,
).Return(nil, errDummy).Once()
// Send the payment and assert it failed.
@ -911,7 +923,7 @@ func TestResumePaymentFailOnRegisterAttemptErr(t *testing.T) {
// 4. mock requestRoute to return an route.
m.paySession.On("RequestRoute",
paymentAmt, p.feeLimit, uint32(ps.NumAttemptsInFlight),
uint32(p.currentHeight),
uint32(p.currentHeight), mock.Anything,
).Return(rt, nil).Once()
// 5. mock shardTracker used in `createNewPaymentAttempt` to return an
@ -971,7 +983,7 @@ func TestResumePaymentFailOnSendAttemptErr(t *testing.T) {
// 4. mock requestRoute to return an route.
m.paySession.On("RequestRoute",
paymentAmt, p.feeLimit, uint32(ps.NumAttemptsInFlight),
uint32(p.currentHeight),
uint32(p.currentHeight), mock.Anything,
).Return(rt, nil).Once()
// 5. mock `registerAttempt` to return an attempt.
@ -1063,7 +1075,7 @@ func TestResumePaymentSuccess(t *testing.T) {
// 1.4. mock requestRoute to return an route.
m.paySession.On("RequestRoute",
paymentAmt, p.feeLimit, uint32(ps.NumAttemptsInFlight),
uint32(p.currentHeight),
uint32(p.currentHeight), mock.Anything,
).Return(rt, nil).Once()
// 1.5. mock `registerAttempt` to return an attempt.
@ -1164,7 +1176,7 @@ func TestResumePaymentSuccessWithTwoAttempts(t *testing.T) {
// 1.4. mock requestRoute to return an route.
m.paySession.On("RequestRoute",
paymentAmt, p.feeLimit, uint32(ps.NumAttemptsInFlight),
uint32(p.currentHeight),
uint32(p.currentHeight), mock.Anything,
).Return(rt, nil).Once()
// Create two attempt IDs here.
@ -1226,7 +1238,7 @@ func TestResumePaymentSuccessWithTwoAttempts(t *testing.T) {
// 2.4. mock requestRoute to return an route.
m.paySession.On("RequestRoute",
paymentAmt/2, p.feeLimit, uint32(ps.NumAttemptsInFlight),
uint32(p.currentHeight),
uint32(p.currentHeight), mock.Anything,
).Return(rt, nil).Once()
// 2.5. mock `registerAttempt` to return an attempt.

View file

@ -139,7 +139,9 @@ type PaymentSession interface {
// A noRouteError is returned if a non-critical error is encountered
// during path finding.
RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
activeShards, height uint32) (*route.Route, error)
activeShards, height uint32,
firstHopCustomRecords lnwire.CustomRecords) (*route.Route,
error)
// UpdateAdditionalEdge takes an additional channel edge policy
// (private channels) and applies the update from the message. Returns
@ -243,7 +245,8 @@ func newPaymentSession(p *LightningPayment, selfNode route.Vertex,
// NOTE: This function is safe for concurrent access.
// NOTE: Part of the PaymentSession interface.
func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
activeShards, height uint32) (*route.Route, error) {
activeShards, height uint32,
firstHopCustomRecords lnwire.CustomRecords) (*route.Route, error) {
if p.empty {
return nil, errEmptyPaySession
@ -284,9 +287,9 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
// client-side MTU that we'll attempt to respect at all times.
maxShardActive := p.payment.MaxShardAmt != nil
if maxShardActive && maxAmt > *p.payment.MaxShardAmt {
p.log.Debug("Clamping payment attempt from %v to %v due to "+
"max shard size of %v", maxAmt,
*p.payment.MaxShardAmt, maxAmt)
p.log.Debugf("Clamping payment attempt from %v to %v due to "+
"max shard size of %v", maxAmt, *p.payment.MaxShardAmt,
maxAmt)
maxAmt = *p.payment.MaxShardAmt
}

View file

@ -4,8 +4,10 @@ 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/tlv"
"github.com/lightningnetwork/lnd/zpay32"
)
@ -49,12 +51,14 @@ type SessionSource struct {
// 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) (
PaymentSession, error) {
func (m *SessionSource) NewPaymentSession(p *LightningPayment,
firstHopBlob fn.Option[tlv.Blob],
trafficShaper fn.Option[TlvTrafficShaper]) (PaymentSession, error) {
getBandwidthHints := func(graph Graph) (bandwidthHints, error) {
return newBandwidthManager(
graph, m.SourceNode.PubKeyBytes, m.GetLink,
firstHopBlob, trafficShaper,
)
}

View file

@ -235,6 +235,9 @@ func TestRequestRoute(t *testing.T) {
route, err := session.RequestRoute(
payment.Amount, payment.FeeLimit, 0, height,
lnwire.CustomRecords{
lnwire.MinCustomRecordsTlvType + 123: []byte{1, 2, 3},
},
)
if err != nil {
t.Fatal(err)

View file

@ -29,6 +29,7 @@ import (
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/routing/shards"
"github.com/lightningnetwork/lnd/tlv"
"github.com/lightningnetwork/lnd/zpay32"
)
@ -154,7 +155,10 @@ 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,
firstHopBlob fn.Option[tlv.Blob],
trafficShaper fn.Option[TlvTrafficShaper]) (PaymentSession,
error)
// NewPaymentSessionEmpty creates a new paymentSession instance that is
// empty, and will be exhausted immediately. Used for failure reporting
@ -290,6 +294,10 @@ type Config struct {
//
// TODO(yy): remove it once the root cause of stuck payments is found.
ClosedSCIDs map[lnwire.ShortChannelID]struct{}
// TrafficShaper is an optional traffic shaper that can be used to
// control the outgoing channel of a payment.
TrafficShaper fn.Option[TlvTrafficShaper]
}
// EdgeLocator is a struct used to identify a specific edge.
@ -517,6 +525,7 @@ func (r *ChannelRouter) FindRoute(req *RouteRequest) (*route.Route, float64,
// eliminate certain routes early on in the path finding process.
bandwidthHints, err := newBandwidthManager(
r.cfg.RoutingGraph, r.cfg.SelfNode, r.cfg.GetLink,
fn.None[tlv.Blob](), r.cfg.TrafficShaper,
)
if err != nil {
return nil, 0, err
@ -1009,10 +1018,29 @@ func spewPayment(payment *LightningPayment) lnutils.LogClosure {
func (r *ChannelRouter) PreparePayment(payment *LightningPayment) (
PaymentSession, shards.ShardTracker, error) {
// Assemble any custom data we want to send to the first hop only.
var firstHopData fn.Option[tlv.Blob]
if len(payment.FirstHopCustomRecords) > 0 {
if err := payment.FirstHopCustomRecords.Validate(); err != nil {
return nil, nil, fmt.Errorf("invalid first hop custom "+
"records: %w", err)
}
firstHopBlob, err := payment.FirstHopCustomRecords.Serialize()
if err != nil {
return nil, nil, fmt.Errorf("unable to serialize "+
"first hop custom records: %w", err)
}
firstHopData = fn.Some(firstHopBlob)
}
// 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, firstHopData, r.cfg.TrafficShaper,
)
if err != nil {
return nil, nil, err
}
@ -1277,6 +1305,11 @@ func (r *ChannelRouter) sendPayment(ctx context.Context,
return [32]byte{}, nil, err
}
// Validate the custom records before we attempt to send the payment.
if err := firstHopCustomRecords.Validate(); err != nil {
return [32]byte{}, nil, err
}
// Now set up a paymentLifecycle struct with these params, such that we
// can resume the payment from the current state.
p := newPaymentLifecycle(
@ -1327,7 +1360,7 @@ func (e ErrNoChannel) Error() string {
// outgoing channel, use the outgoingChan parameter.
func (r *ChannelRouter) BuildRoute(amt fn.Option[lnwire.MilliSatoshi],
hops []route.Vertex, outgoingChan *uint64, finalCltvDelta int32,
payAddr *[32]byte, _ fn.Option[[]byte]) (*route.Route,
payAddr *[32]byte, firstHopBlob fn.Option[[]byte]) (*route.Route,
error) {
log.Tracef("BuildRoute called: hopsCount=%v, amt=%v", len(hops), amt)
@ -1342,7 +1375,8 @@ func (r *ChannelRouter) BuildRoute(amt fn.Option[lnwire.MilliSatoshi],
// We'll attempt to obtain a set of bandwidth hints that helps us select
// the best outgoing channel to use in case no outgoing channel is set.
bandwidthHints, err := newBandwidthManager(
r.cfg.RoutingGraph, r.cfg.SelfNode, r.cfg.GetLink,
r.cfg.RoutingGraph, r.cfg.SelfNode, r.cfg.GetLink, firstHopBlob,
r.cfg.TrafficShaper,
)
if err != nil {
return nil, err

View file

@ -164,6 +164,9 @@ func createTestCtxFromGraphInstanceAssumeValid(t *testing.T,
Clock: clock.NewTestClock(time.Unix(1, 0)),
ApplyChannelUpdate: graphBuilder.ApplyChannelUpdate,
ClosedSCIDs: mockClosedSCIDs,
TrafficShaper: fn.Some[TlvTrafficShaper](
&mockTrafficShaper{},
),
})
require.NoError(t, router.Start(), "unable to start router")
@ -2190,6 +2193,7 @@ func TestSendToRouteSkipTempErrSuccess(t *testing.T) {
return 0, nil
},
ClosedSCIDs: mockClosedSCIDs,
TrafficShaper: fn.Some[TlvTrafficShaper](&mockTrafficShaper{}),
}}
// Register mockers with the expected method calls.
@ -2274,6 +2278,7 @@ func TestSendToRouteSkipTempErrNonMPP(t *testing.T) {
return 0, nil
},
ClosedSCIDs: mockClosedSCIDs,
TrafficShaper: fn.Some[TlvTrafficShaper](&mockTrafficShaper{}),
}}
// Expect an error to be returned.
@ -2329,6 +2334,7 @@ func TestSendToRouteSkipTempErrTempFailure(t *testing.T) {
return 0, nil
},
ClosedSCIDs: mockClosedSCIDs,
TrafficShaper: fn.Some[TlvTrafficShaper](&mockTrafficShaper{}),
}}
// Create the error to be returned.
@ -2412,6 +2418,7 @@ func TestSendToRouteSkipTempErrPermanentFailure(t *testing.T) {
return 0, nil
},
ClosedSCIDs: mockClosedSCIDs,
TrafficShaper: fn.Some[TlvTrafficShaper](&mockTrafficShaper{}),
}}
// Create the error to be returned.
@ -2499,6 +2506,7 @@ func TestSendToRouteTempFailure(t *testing.T) {
return 0, nil
},
ClosedSCIDs: mockClosedSCIDs,
TrafficShaper: fn.Some[TlvTrafficShaper](&mockTrafficShaper{}),
}}
// Create the error to be returned.