Merge pull request #7852 from ziggie1984/pathfind_sizeestimate

Path Finding Size Estimation including Blinded Path Data
This commit is contained in:
Yong authored and GitHub committed 2024-02-05 15:21:51 +08:00
commit d41b86bb7e
19 files changed
+809 -79

No files matched your search

@@ -78,6 +78,9 @@
* [Add Taproot witness types * [Add Taproot witness types
to rpc](https://github.com/lightningnetwork/lnd/pull/8431) to rpc](https://github.com/lightningnetwork/lnd/pull/8431)
* [Fixed](https://github.com/lightningnetwork/lnd/pull/7852) the payload size
calculation in our pathfinder because blinded hops introduced new tlv records.
# New Features # New Features
## Functional Enhancements ## Functional Enhancements
+4
View File
@@ -104,6 +104,10 @@ func testSendPaymentAMPInvoiceCase(ht *lntest.HarnessTest,
// expect an extra invoice to appear in the ListInvoices response, since // expect an extra invoice to appear in the ListInvoices response, since
// a new invoice will be JIT inserted under a different payment address // a new invoice will be JIT inserted under a different payment address
// than the one in the invoice. // than the one in the invoice.
//
// NOTE: This will only work when the peer has spontaneous AMP payments
// enabled otherwise no invoice under a different payment_addr will be
// found.
var ( var (
expNumInvoices = 1 expNumInvoices = 1
externalPayAddr []byte externalPayAddr []byte
+5 -2
View File
@@ -15,7 +15,6 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
sphinx "github.com/lightningnetwork/lightning-onion" sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/feature" "github.com/lightningnetwork/lnd/feature"
"github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
@@ -280,7 +279,7 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
// inside of the path rather than the request's fields. // inside of the path rather than the request's fields.
var ( var (
targetPubKey *route.Vertex targetPubKey *route.Vertex
routeHintEdges map[route.Vertex][]*models.CachedEdgePolicy routeHintEdges map[route.Vertex][]routing.AdditionalEdge
blindedPmt *routing.BlindedPayment blindedPmt *routing.BlindedPayment
// finalCLTVDelta varies depending on whether we're sending to // finalCLTVDelta varies depending on whether we're sending to
@@ -391,6 +390,7 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
DestCustomRecords: record.CustomSet(in.DestCustomRecords), DestCustomRecords: record.CustomSet(in.DestCustomRecords),
CltvLimit: cltvLimit, CltvLimit: cltvLimit,
DestFeatures: destinationFeatures, DestFeatures: destinationFeatures,
BlindedPayment: blindedPmt,
} }
// Pass along an outgoing channel restriction if specified. // Pass along an outgoing channel restriction if specified.
@@ -967,6 +967,9 @@ func (r *RouterBackend) extractIntentFromSendRequest(
// pseudo-reusable, e.g. the invoice parameters are // pseudo-reusable, e.g. the invoice parameters are
// reused (amt, cltv, hop hints, etc) even though the // reused (amt, cltv, hop hints, etc) even though the
// payments will share different payment hashes. // payments will share different payment hashes.
//
// NOTE: This will only work when the peer has
// spontaneous AMP payments enabled.
if len(rpcPayReq.PaymentAddr) > 0 { if len(rpcPayReq.PaymentAddr) > 0 {
var addr [32]byte var addr [32]byte
copy(addr[:], rpcPayReq.PaymentAddr) copy(addr[:], rpcPayReq.PaymentAddr)
+10
View File
@@ -19,6 +19,16 @@ type AMP struct {
childIndex uint32 childIndex uint32
} }
// MaxAmpPayLoadSize is an AMP Record which when serialized to a tlv record uses
// the maximum payload size. The `childIndex` is created randomly and is a
// 4 byte `varint` type so we make sure we use an index which will be encoded in
// 4 bytes.
var MaxAmpPayLoadSize = AMP{
rootShare: [32]byte{},
setID: [32]byte{},
childIndex: 0x80000000,
}
// NewAMP generate a new AMP record with the given root_share, set_id, and // NewAMP generate a new AMP record with the given root_share, set_id, and
// child_index. // child_index.
func NewAMP(rootShare, setID [32]byte, childIndex uint32) *AMP { func NewAMP(rootShare, setID [32]byte, childIndex uint32) *AMP {
+107
View File
@@ -0,0 +1,107 @@
package routing
import (
"errors"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
)
var (
// ErrNoPayLoadSizeFunc is returned when no payload size function is
// definied.
ErrNoPayLoadSizeFunc = errors.New("no payloadSizeFunc defined for " +
"additional edge")
)
// AdditionalEdge is an interface which specifies additional edges which can
// be appended to an existing route. Compared to normal edges of a route they
// provide an explicit payload size function and are introduced because blinded
// paths differ in their payload structure.
type AdditionalEdge interface {
// IntermediatePayloadSize returns the size of the payload for the
// additional edge when being an intermediate hop in a route NOT the
// final hop.
IntermediatePayloadSize(amount lnwire.MilliSatoshi, expiry uint32,
legacy bool, channelID uint64) uint64
// EdgePolicy returns the policy of the additional edge.
EdgePolicy() *models.CachedEdgePolicy
}
// PayloadSizeFunc defines the interface for the payload size function.
type PayloadSizeFunc func(amount lnwire.MilliSatoshi, expiry uint32,
legacy bool, channelID uint64) uint64
// PrivateEdge implements the AdditionalEdge interface. As the name implies it
// is used for private route hints that the receiver adds for example to an
// invoice.
type PrivateEdge struct {
policy *models.CachedEdgePolicy
}
// EdgePolicy return the policy of the PrivateEdge.
func (p *PrivateEdge) EdgePolicy() *models.CachedEdgePolicy {
return p.policy
}
// IntermediatePayloadSize returns the sphinx payload size defined in BOLT04 if
// this edge were to be included in a route.
func (p *PrivateEdge) IntermediatePayloadSize(amount lnwire.MilliSatoshi,
expiry uint32, legacy bool, channelID uint64) uint64 {
hop := route.Hop{
AmtToForward: amount,
OutgoingTimeLock: expiry,
LegacyPayload: legacy,
}
return hop.PayloadSize(channelID)
}
// BlindedEdge implements the AdditionalEdge interface. Blinded hops are viewed
// as additional edges because they are appened at the end of a normal route.
type BlindedEdge struct {
policy *models.CachedEdgePolicy
cipherText []byte
blindingPoint *btcec.PublicKey
}
// EdgePolicy return the policy of the BlindedEdge.
func (b *BlindedEdge) EdgePolicy() *models.CachedEdgePolicy {
return b.policy
}
// IntermediatePayloadSize returns the sphinx payload size defined in BOLT04 if
// this edge were to be included in a route.
func (b *BlindedEdge) IntermediatePayloadSize(_ lnwire.MilliSatoshi, _ uint32,
_ bool, _ uint64) uint64 {
hop := route.Hop{
BlindingPoint: b.blindingPoint,
LegacyPayload: false,
EncryptedData: b.cipherText,
}
// For blinded paths the next chanID is in the encrypted data tlv.
return hop.PayloadSize(0)
}
// Compile-time constraints to ensure the PrivateEdge and the BlindedEdge
// implement the AdditionalEdge interface.
var _ AdditionalEdge = (*PrivateEdge)(nil)
var _ AdditionalEdge = (*BlindedEdge)(nil)
// defaultHopPayloadSize is the default payload size of a normal (not-blinded)
// hop in the route.
func defaultHopPayloadSize(amount lnwire.MilliSatoshi, expiry uint32,
legacy bool, channelID uint64) uint64 {
// The payload size of a cleartext intermediate hop is equal to the
// payload size of a private edge therefore we reuse its size function.
edge := PrivateEdge{}
return edge.IntermediatePayloadSize(amount, expiry, legacy, channelID)
}
+136
View File
@@ -0,0 +1,136 @@
package routing
import (
"bytes"
"encoding/binary"
"encoding/hex"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
)
// TestIntermediatePayloadSize tests the payload size functions of the
// PrivateEdge and the BlindedEdge.
func TestIntermediatePayloadSize(t *testing.T) {
t.Parallel()
testPrivKeyBytes, _ := hex.DecodeString("e126f68f7eafcc8b74f54d269fe" +
"206be715000f94dac067d1c04a8ca3b2db734")
_, blindedPoint := btcec.PrivKeyFromBytes(testPrivKeyBytes)
testCases := []struct {
name string
hop route.Hop
nextHop uint64
edge AdditionalEdge
}{
{
name: "Legacy payload private edge",
hop: route.Hop{
AmtToForward: 1000,
OutgoingTimeLock: 600000,
ChannelID: 3432483437438,
LegacyPayload: true,
},
nextHop: 1,
edge: &PrivateEdge{},
},
{
name: "Tlv payload private edge",
hop: route.Hop{
AmtToForward: 1000,
OutgoingTimeLock: 600000,
ChannelID: 3432483437438,
LegacyPayload: false,
},
nextHop: 1,
edge: &PrivateEdge{},
},
{
name: "Blinded edge",
hop: route.Hop{
EncryptedData: []byte{12, 13},
},
edge: &BlindedEdge{
cipherText: []byte{12, 13},
},
},
{
name: "Blinded edge - introduction point",
hop: route.Hop{
EncryptedData: []byte{12, 13},
BlindingPoint: blindedPoint,
},
edge: &BlindedEdge{
cipherText: []byte{12, 13},
blindingPoint: blindedPoint,
},
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
payLoad, err := createHopPayload(
testCase.hop, testCase.nextHop, false,
)
require.NoErrorf(t, err, "failed to create hop payload")
expectedPayloadSize := testCase.edge.
IntermediatePayloadSize(
testCase.hop.AmtToForward,
testCase.hop.OutgoingTimeLock,
testCase.hop.LegacyPayload,
testCase.nextHop,
)
require.Equal(
t, expectedPayloadSize,
uint64(payLoad.NumBytes()),
)
})
}
}
// createHopPayload creates the hop payload of the sphinx package to facilitate
// the testing of the payload size.
func createHopPayload(hop route.Hop, nextHop uint64,
finalHop bool) (sphinx.HopPayload, error) {
// If this is the legacy payload, then we can just include the
// hop data as normal.
if hop.LegacyPayload {
// Before we encode this value, we'll pack the next hop
// into the NextAddress field of the hop info to ensure
// we point to the right now.
hopData := sphinx.HopData{
ForwardAmount: uint64(hop.AmtToForward),
OutgoingCltv: hop.OutgoingTimeLock,
}
binary.BigEndian.PutUint64(
hopData.NextAddress[:], nextHop,
)
return sphinx.NewLegacyHopPayload(&hopData)
}
// For non-legacy payloads, we'll need to pack the
// routing information, along with any extra TLV
// information into the new per-hop payload format.
// We'll also pass in the chan ID of the hop this
// channel should be forwarded to so we can construct a
// valid payload.
var b bytes.Buffer
err := hop.PackHopPayload(&b, nextHop, finalHop)
if err != nil {
return sphinx.HopPayload{}, err
}
return sphinx.NewTLVHopPayload(b.Bytes())
}
+16 -6
View File
@@ -99,7 +99,7 @@ func (b *BlindedPayment) toRouteHints() RouteHints {
hintCount := len(b.BlindedPath.BlindedHops) - 1 hintCount := len(b.BlindedPath.BlindedHops) - 1
hints := make( hints := make(
map[route.Vertex][]*models.CachedEdgePolicy, hintCount, RouteHints, hintCount,
) )
// Start at the unblinded introduction node, because our pathfinding // Start at the unblinded introduction node, because our pathfinding
@@ -116,8 +116,7 @@ func (b *BlindedPayment) toRouteHints() RouteHints {
// will ensure that pathfinding provides sufficient fees/delay for the // will ensure that pathfinding provides sufficient fees/delay for the
// blinded portion to the introduction node. // blinded portion to the introduction node.
firstBlindedHop := b.BlindedPath.BlindedHops[1].BlindedNodePub firstBlindedHop := b.BlindedPath.BlindedHops[1].BlindedNodePub
hints[fromNode] = []*models.CachedEdgePolicy{ edgePolicy := &models.CachedEdgePolicy{
{
TimeLockDelta: b.CltvExpiryDelta, TimeLockDelta: b.CltvExpiryDelta,
MinHTLC: lnwire.MilliSatoshi(b.HtlcMinimum), MinHTLC: lnwire.MilliSatoshi(b.HtlcMinimum),
MaxHTLC: lnwire.MilliSatoshi(b.HtlcMaximum), MaxHTLC: lnwire.MilliSatoshi(b.HtlcMaximum),
@@ -135,6 +134,13 @@ func (b *BlindedPayment) toRouteHints() RouteHints {
) )
}, },
ToNodeFeatures: features, ToNodeFeatures: features,
}
hints[fromNode] = []AdditionalEdge{
&BlindedEdge{
policy: edgePolicy,
cipherText: b.BlindedPath.BlindedHops[0].CipherText,
blindingPoint: b.BlindedPath.BlindingPoint,
}, },
} }
@@ -156,15 +162,19 @@ func (b *BlindedPayment) toRouteHints() RouteHints {
b.BlindedPath.BlindedHops[nextHopIdx].BlindedNodePub, b.BlindedPath.BlindedHops[nextHopIdx].BlindedNodePub,
) )
hint := &models.CachedEdgePolicy{ edgePolicy := &models.CachedEdgePolicy{
ToNodePubKey: func() route.Vertex { ToNodePubKey: func() route.Vertex {
return nextNode return nextNode
}, },
ToNodeFeatures: features, ToNodeFeatures: features,
} }
hints[fromNode] = []*models.CachedEdgePolicy{ hints[fromNode] = []AdditionalEdge{
hint, &BlindedEdge{
policy: edgePolicy,
cipherText: b.BlindedPath.BlindedHops[i].
CipherText,
},
} }
} }
+39 -7
View File
@@ -1,10 +1,12 @@
package routing package routing
import ( import (
"bytes"
"testing" "testing"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion" sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -94,6 +96,12 @@ func TestBlindedPaymentToHints(t *testing.T) {
htlcMin uint64 = 100 htlcMin uint64 = 100
htlcMax uint64 = 100_000_000 htlcMax uint64 = 100_000_000
sizeEncryptedData = 100
cipherText = bytes.Repeat(
[]byte{1}, sizeEncryptedData,
)
_, blindedPoint = btcec.PrivKeyFromBytes([]byte{5})
rawFeatures = lnwire.NewRawFeatureVector( rawFeatures = lnwire.NewRawFeatureVector(
lnwire.AMPOptional, lnwire.AMPOptional,
) )
@@ -108,6 +116,7 @@ func TestBlindedPaymentToHints(t *testing.T) {
blindedPayment := &BlindedPayment{ blindedPayment := &BlindedPayment{
BlindedPath: &sphinx.BlindedPath{ BlindedPath: &sphinx.BlindedPath{
IntroductionPoint: pk1, IntroductionPoint: pk1,
BlindingPoint: blindedPoint,
BlindedHops: []*sphinx.BlindedHopInfo{ BlindedHops: []*sphinx.BlindedHopInfo{
{}, {},
}, },
@@ -125,18 +134,23 @@ func TestBlindedPaymentToHints(t *testing.T) {
blindedPayment.BlindedPath.BlindedHops = []*sphinx.BlindedHopInfo{ blindedPayment.BlindedPath.BlindedHops = []*sphinx.BlindedHopInfo{
{ {
BlindedNodePub: pkb1, BlindedNodePub: pkb1,
CipherText: cipherText,
}, },
{ {
BlindedNodePub: pkb2, BlindedNodePub: pkb2,
CipherText: cipherText,
}, },
{ {
BlindedNodePub: pkb3, BlindedNodePub: pkb3,
CipherText: cipherText,
}, },
} }
expected := RouteHints{ expected := RouteHints{
v1: { v1: {
{ //nolint:lll
&BlindedEdge{
policy: &models.CachedEdgePolicy{
TimeLockDelta: cltvDelta, TimeLockDelta: cltvDelta,
MinHTLC: lnwire.MilliSatoshi(htlcMin), MinHTLC: lnwire.MilliSatoshi(htlcMin),
MaxHTLC: lnwire.MilliSatoshi(htlcMax), MaxHTLC: lnwire.MilliSatoshi(htlcMax),
@@ -149,16 +163,23 @@ func TestBlindedPaymentToHints(t *testing.T) {
}, },
ToNodeFeatures: features, ToNodeFeatures: features,
}, },
blindingPoint: blindedPoint,
cipherText: cipherText,
},
}, },
vb2: { vb2: {
{ &BlindedEdge{
policy: &models.CachedEdgePolicy{
ToNodePubKey: func() route.Vertex { ToNodePubKey: func() route.Vertex {
return vb3 return vb3
}, },
ToNodeFeatures: features, ToNodeFeatures: features,
}, },
cipherText: cipherText,
},
}, },
} }
actual := blindedPayment.toRouteHints() actual := blindedPayment.toRouteHints()
require.Equal(t, len(expected), len(actual)) require.Equal(t, len(expected), len(actual))
@@ -170,13 +191,24 @@ func TestBlindedPaymentToHints(t *testing.T) {
require.Len(t, actualHint, 1) require.Len(t, actualHint, 1)
// We can't assert that our functions are equal, so we check // We can't assert that our functions are equal, so we check
// their output and then mark as nil so that we can use // their output and then mark them as nil so that we can use
// require.Equal for all our other fields. // require.Equal for all our other fields.
require.Equal(t, expectedHint[0].ToNodePubKey(), require.Equal(t, expectedHint[0].EdgePolicy().ToNodePubKey(),
actualHint[0].ToNodePubKey()) actualHint[0].EdgePolicy().ToNodePubKey())
actualHint[0].ToNodePubKey = nil actualHint[0].EdgePolicy().ToNodePubKey = nil
expectedHint[0].ToNodePubKey = nil expectedHint[0].EdgePolicy().ToNodePubKey = nil
// The arguments we use for the payload do not matter as long as
// both functions return the same payload.
expectedPayloadSize := expectedHint[0].IntermediatePayloadSize(
0, 0, false, 0,
)
actualPayloadSize := actualHint[0].IntermediatePayloadSize(
0, 0, false, 0,
)
require.Equal(t, expectedPayloadSize, actualPayloadSize)
require.Equal(t, expectedHint[0], actualHint[0]) require.Equal(t, expectedHint[0], actualHint[0])
} }
+32
View File
@@ -0,0 +1,32 @@
package routing
import (
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/mock"
)
// mockAdditionalEdge is a mock of the AdditionalEdge interface.
type mockAdditionalEdge struct{ mock.Mock }
// IntermediatePayloadSize returns the sphinx payload size defined in BOLT04 if
// this edge were to be included in a route.
func (m *mockAdditionalEdge) IntermediatePayloadSize(amount lnwire.MilliSatoshi,
expiry uint32, legacy bool, channelID uint64) uint64 {
args := m.Called(amount, expiry, legacy, channelID)
return args.Get(0).(uint64)
}
// EdgePolicy return the policy of the mockAdditionalEdge.
func (m *mockAdditionalEdge) EdgePolicy() *models.CachedEdgePolicy {
args := m.Called()
edgePolicy := args.Get(0)
if edgePolicy == nil {
return nil
}
return edgePolicy.(*models.CachedEdgePolicy)
}
+97 -35
View File
@@ -88,7 +88,7 @@ var (
// of the edge. // of the edge.
type edgePolicyWithSource struct { type edgePolicyWithSource struct {
sourceNode route.Vertex sourceNode route.Vertex
edge *models.CachedEdgePolicy edge AdditionalEdge
} }
// finalHopParams encapsulates various parameters for route construction that // finalHopParams encapsulates various parameters for route construction that
@@ -355,8 +355,9 @@ type graphParams struct {
// additionalEdges is an optional set of edges that should be // additionalEdges is an optional set of edges that should be
// considered during path finding, that is not already found in the // considered during path finding, that is not already found in the
// channel graph. // channel graph. These can either be private edges for bolt 11 invoices
additionalEdges map[route.Vertex][]*models.CachedEdgePolicy // or blinded edges when a payment to a blinded path is made.
additionalEdges map[route.Vertex][]AdditionalEdge
// bandwidthHints is an interface that provides bandwidth hints that // bandwidthHints is an interface that provides bandwidth hints that
// can provide a better estimate of the current channel bandwidth than // can provide a better estimate of the current channel bandwidth than
@@ -407,9 +408,18 @@ type RestrictParams struct {
// invoices. // invoices.
PaymentAddr *[32]byte PaymentAddr *[32]byte
// Amp signals to the pathfinder that this payment is an AMP payment
// and therefore it needs to account for additional AMP data in the
// final hop payload size calculation.
Amp *AMPOptions
// Metadata is additional data that is sent along with the payment to // Metadata is additional data that is sent along with the payment to
// the payee. // the payee.
Metadata []byte Metadata []byte
// BlindedPayment is necessary to determine the hop size of the
// last/exit hop.
BlindedPayment *BlindedPayment
} }
// PathFindingConfig defines global parameters that control the trade-off in // PathFindingConfig defines global parameters that control the trade-off in
@@ -609,7 +619,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
distance := make(map[route.Vertex]*nodeWithDist, estimatedNodeCount) distance := make(map[route.Vertex]*nodeWithDist, estimatedNodeCount)
additionalEdgesWithSrc := make(map[route.Vertex][]*edgePolicyWithSource) additionalEdgesWithSrc := make(map[route.Vertex][]*edgePolicyWithSource)
for vertex, outgoingEdgePolicies := range g.additionalEdges { for vertex, additionalEdges := range g.additionalEdges {
// Edges connected to self are always included in the graph, // Edges connected to self are always included in the graph,
// therefore can be skipped. This prevents us from trying // therefore can be skipped. This prevents us from trying
// routes to malformed hop hints. // routes to malformed hop hints.
@@ -619,12 +629,13 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// Build reverse lookup to find incoming edges. Needed because // Build reverse lookup to find incoming edges. Needed because
// search is taken place from target to source. // search is taken place from target to source.
for _, outgoingEdgePolicy := range outgoingEdgePolicies { for _, additionalEdge := range additionalEdges {
outgoingEdgePolicy := additionalEdge.EdgePolicy()
toVertex := outgoingEdgePolicy.ToNodePubKey() toVertex := outgoingEdgePolicy.ToNodePubKey()
incomingEdgePolicy := &edgePolicyWithSource{ incomingEdgePolicy := &edgePolicyWithSource{
sourceNode: vertex, sourceNode: vertex,
edge: outgoingEdgePolicy, edge: additionalEdge,
} }
additionalEdgesWithSrc[toVertex] = additionalEdgesWithSrc[toVertex] =
@@ -633,23 +644,10 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
} }
} }
// Build a preliminary destination hop structure to obtain the payload // The payload size of the final hop differ from intermediate hops
// size. // and depends on whether the destination is blinded or not.
var mpp *record.MPP lastHopPayloadSize := lastHopPayloadSize(r, finalHtlcExpiry, amt,
if r.PaymentAddr != nil { !features.HasFeature(lnwire.TLVOnionPayloadOptional))
mpp = record.NewMPP(amt, *r.PaymentAddr)
}
finalHop := route.Hop{
AmtToForward: amt,
OutgoingTimeLock: uint32(finalHtlcExpiry),
CustomRecords: r.DestCustomRecords,
LegacyPayload: !features.HasFeature(
lnwire.TLVOnionPayloadOptional,
),
MPP: mpp,
Metadata: r.Metadata,
}
// We can't always assume that the end destination is publicly // We can't always assume that the end destination is publicly
// advertised to the network so we'll manually include the target node. // advertised to the network so we'll manually include the target node.
@@ -667,7 +665,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
amountToReceive: amt, amountToReceive: amt,
incomingCltv: finalHtlcExpiry, incomingCltv: finalHtlcExpiry,
probability: 1, probability: 1,
routingInfoSize: finalHop.PayloadSize(0), routingInfoSize: lastHopPayloadSize,
} }
// Calculate the absolute cltv limit. Use uint64 to prevent an overflow // Calculate the absolute cltv limit. Use uint64 to prevent an overflow
@@ -821,23 +819,30 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// blob. // blob.
var payloadSize uint64 var payloadSize uint64
if fromVertex != source { if fromVertex != source {
// In case the unifiedEdge does not have a payload size
// function supplied we request a graceful shutdown
// because this should never happen.
if edge.hopPayloadSizeFn == nil {
log.Criticalf("No payload size function "+
"available for edge=%v unable to "+
"determine payload size: %v", edge,
ErrNoPayLoadSizeFunc)
return
}
supportsTlv := fromFeatures.HasFeature( supportsTlv := fromFeatures.HasFeature(
lnwire.TLVOnionPayloadOptional, lnwire.TLVOnionPayloadOptional,
) )
hop := route.Hop{ payloadSize = edge.hopPayloadSizeFn(
AmtToForward: amountToSend, amountToSend,
OutgoingTimeLock: uint32( uint32(toNodeDist.incomingCltv),
toNodeDist.incomingCltv, !supportsTlv, edge.policy.ChannelID,
), )
LegacyPayload: !supportsTlv,
}
payloadSize = hop.PayloadSize(edge.policy.ChannelID)
} }
routingInfoSize := toNodeDist.routingInfoSize + payloadSize routingInfoSize := toNodeDist.routingInfoSize + payloadSize
// Skip paths that would exceed the maximum routing info size. // Skip paths that would exceed the maximum routing info size.
if routingInfoSize > sphinx.MaxPayloadSize { if routingInfoSize > sphinx.MaxPayloadSize {
return return
@@ -930,9 +935,14 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// calculations. We set a high capacity to act as if // calculations. We set a high capacity to act as if
// there is enough liquidity, otherwise the hint would // there is enough liquidity, otherwise the hint would
// not have been added by a wallet. // not have been added by a wallet.
// We also pass the payload size function to the
// graph data so that we calculate the exact payload
// size when evaluating this hop for a route.
u.addPolicy( u.addPolicy(
reverseEdge.sourceNode, reverseEdge.edge, reverseEdge.sourceNode,
reverseEdge.edge.EdgePolicy(),
fakeHopHintCapacity, fakeHopHintCapacity,
reverseEdge.edge.IntermediatePayloadSize,
) )
} }
@@ -1098,3 +1108,55 @@ func getProbabilityBasedDist(weight int64, probability float64,
return int64(dist) return int64(dist)
} }
// lastHopPayloadSize calculates the payload size of the final hop in a route.
// It depends on the tlv types which are present and also whether the hop is
// part of a blinded route or not.
func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
amount lnwire.MilliSatoshi, legacy bool) uint64 {
if r.BlindedPayment != nil {
blindedPath := r.BlindedPayment.BlindedPath.BlindedHops
blindedPoint := r.BlindedPayment.BlindedPath.BlindingPoint
encryptedData := blindedPath[len(blindedPath)-1].CipherText
finalHop := route.Hop{
AmtToForward: amount,
OutgoingTimeLock: uint32(finalHtlcExpiry),
LegacyPayload: false,
EncryptedData: encryptedData,
}
if len(blindedPath) == 1 {
finalHop.BlindingPoint = blindedPoint
}
// The final hop does not have a short chanID set.
return finalHop.PayloadSize(0)
}
var mpp *record.MPP
if r.PaymentAddr != nil {
mpp = record.NewMPP(amount, *r.PaymentAddr)
}
var amp *record.AMP
if r.Amp != nil {
// The AMP payload is not easy accessible at this point but we
// are only interested in the size of the payload so we just use
// the AMP record dummy.
amp = &record.MaxAmpPayLoadSize
}
finalHop := route.Hop{
AmtToForward: amount,
OutgoingTimeLock: uint32(finalHtlcExpiry),
CustomRecords: r.DestCustomRecords,
LegacyPayload: legacy,
MPP: mpp,
AMP: amp,
Metadata: r.Metadata,
}
// The final hop does not have a short chanID set.
return finalHop.PayloadSize(0)
}
+299 -10
View File
@@ -746,6 +746,9 @@ func TestPathFinding(t *testing.T) {
}, { }, {
name: "path finding with additional edges", name: "path finding with additional edges",
fn: runPathFindingWithAdditionalEdges, fn: runPathFindingWithAdditionalEdges,
}, {
name: "path finding max payload restriction",
fn: runPathFindingMaxPayloadRestriction,
}, { }, {
name: "path finding with redundant additional edges", name: "path finding with redundant additional edges",
fn: runPathFindingWithRedundantAdditionalEdges, fn: runPathFindingWithRedundantAdditionalEdges,
@@ -1204,7 +1207,7 @@ func runPathFindingWithAdditionalEdges(t *testing.T, useCache bool) {
// Create the channel edge going from songoku to doge and include it in // Create the channel edge going from songoku to doge and include it in
// our map of additional edges. // our map of additional edges.
songokuToDoge := &models.CachedEdgePolicy{ songokuToDogePolicy := &models.CachedEdgePolicy{
ToNodePubKey: func() route.Vertex { ToNodePubKey: func() route.Vertex {
return doge.PubKeyBytes return doge.PubKeyBytes
}, },
@@ -1215,8 +1218,10 @@ func runPathFindingWithAdditionalEdges(t *testing.T, useCache bool) {
TimeLockDelta: 9, TimeLockDelta: 9,
} }
additionalEdges := map[route.Vertex][]*models.CachedEdgePolicy{ additionalEdges := map[route.Vertex][]AdditionalEdge{
graph.aliasMap["songoku"]: {songokuToDoge}, graph.aliasMap["songoku"]: {&PrivateEdge{
policy: songokuToDogePolicy,
}},
} }
find := func(r *RestrictParams) ( find := func(r *RestrictParams) (
@@ -1266,6 +1271,122 @@ func runPathFindingWithAdditionalEdges(t *testing.T, useCache bool) {
assertExpectedPath(t, graph.aliasMap, path, "songoku", "doge") assertExpectedPath(t, graph.aliasMap, path, "songoku", "doge")
} }
// runPathFindingMaxPayloadRestriction tests the maximum size of a sphinx
// package when creating a route. So we make sure the pathfinder does not return
// a route which is greater than the maximum sphinx package size of 1300 bytes
// defined in BOLT04.
func runPathFindingMaxPayloadRestriction(t *testing.T, useCache bool) {
graph, err := parseTestGraph(t, useCache, basicGraphFilePath)
require.NoError(t, err, "unable to create graph")
sourceNode, err := graph.graph.SourceNode()
require.NoError(t, err, "unable to fetch source node")
paymentAmt := lnwire.NewMSatFromSatoshis(100)
// Create a node doge which is not visible in the graph.
dogePubKeyHex := "03dd46ff29a6941b4a2607525b043ec9b020b3f318a1bf281" +
"536fd7011ec59c882"
dogePubKeyBytes, err := hex.DecodeString(dogePubKeyHex)
require.NoError(t, err, "unable to decode public key")
dogePubKey, err := btcec.ParsePubKey(dogePubKeyBytes)
require.NoError(t, err, "unable to parse public key from bytes")
doge := &channeldb.LightningNode{}
doge.AddPubKey(dogePubKey)
doge.Alias = "doge"
copy(doge.PubKeyBytes[:], dogePubKeyBytes)
graph.aliasMap["doge"] = doge.PubKeyBytes
const (
chanID uint64 = 1337
finalHtlcExpiry int32 = 0
)
// Create the channel edge going from songoku to doge and later add it
// with the mocked size function to the graph data.
songokuToDogePolicy := &models.CachedEdgePolicy{
ToNodePubKey: func() route.Vertex {
return doge.PubKeyBytes
},
ToNodeFeatures: lnwire.EmptyFeatureVector(),
ChannelID: chanID,
FeeBaseMSat: 1,
FeeProportionalMillionths: 1000,
TimeLockDelta: 9,
}
// The route has 2 hops. The exit hop (doge) and the hop
// (songoku -> doge). The desired path looks like this:
// source -> songoku -> doge
tests := []struct {
name string
mockedPayloadSize uint64
err error
}{
{
// The final hop payload size needs to be considered
// as well and because it's treated differently than the
// intermediate hops the following tests choose to use
// the legacy payload format to have a constant final
// hop payload size.
name: "route max payload size (1300)",
mockedPayloadSize: 1300 - sphinx.LegacyHopDataSize,
},
{
// We increase the enrypted data size by one byte.
name: "route 1 bytes bigger than max " +
"payload",
mockedPayloadSize: 1300 - sphinx.LegacyHopDataSize + 1,
err: errNoPathFound,
},
}
for _, testCase := range tests {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
restrictions := *noRestrictions
// No tlv payload, this makes sure the final hop uses
// the legacy payload.
restrictions.DestFeatures = lnwire.EmptyFeatureVector()
// Create the mocked AdditionalEdge and mock the
// corresponding calls.
mockedEdge := &mockAdditionalEdge{}
mockedEdge.On("EdgePolicy").Return(songokuToDogePolicy)
mockedEdge.On("IntermediatePayloadSize",
paymentAmt, uint32(finalHtlcExpiry), true,
chanID).Once().
Return(testCase.mockedPayloadSize)
additionalEdges := map[route.Vertex][]AdditionalEdge{
graph.aliasMap["songoku"]: {mockedEdge},
}
path, err := dbFindPath(
graph.graph, additionalEdges,
&mockBandwidthHints{}, &restrictions,
testPathFindingConfig, sourceNode.PubKeyBytes,
doge.PubKeyBytes, paymentAmt, 0,
finalHtlcExpiry,
)
require.ErrorIs(t, err, testCase.err)
if err == nil {
assertExpectedPath(t, graph.aliasMap, path,
"songoku", "doge")
}
mockedEdge.AssertExpectations(t)
})
}
}
// runPathFindingWithRedundantAdditionalEdges asserts that we are able to find // runPathFindingWithRedundantAdditionalEdges asserts that we are able to find
// paths to nodes ignoring additional edges that are already known by self node. // paths to nodes ignoring additional edges that are already known by self node.
func runPathFindingWithRedundantAdditionalEdges(t *testing.T, useCache bool) { func runPathFindingWithRedundantAdditionalEdges(t *testing.T, useCache bool) {
@@ -1290,7 +1411,7 @@ func runPathFindingWithRedundantAdditionalEdges(t *testing.T, useCache bool) {
// Create the channel edge going from alice to bob and include it in // Create the channel edge going from alice to bob and include it in
// our map of additional edges. // our map of additional edges.
aliceToBob := &models.CachedEdgePolicy{ aliceToBobPolicy := &models.CachedEdgePolicy{
ToNodePubKey: func() route.Vertex { ToNodePubKey: func() route.Vertex {
return target return target
}, },
@@ -1301,8 +1422,10 @@ func runPathFindingWithRedundantAdditionalEdges(t *testing.T, useCache bool) {
TimeLockDelta: 9, TimeLockDelta: 9,
} }
additionalEdges := map[route.Vertex][]*models.CachedEdgePolicy{ additionalEdges := map[route.Vertex][]AdditionalEdge{
ctx.source: {aliceToBob}, ctx.source: {&PrivateEdge{
policy: aliceToBobPolicy,
}},
} }
path, err := dbFindPath( path, err := dbFindPath(
@@ -2402,7 +2525,8 @@ func assertExpectedPath(t *testing.T, aliasMap map[string]route.Vertex,
path []*models.CachedEdgePolicy, nodeAliases ...string) { path []*models.CachedEdgePolicy, nodeAliases ...string) {
if len(path) != len(nodeAliases) { if len(path) != len(nodeAliases) {
t.Fatal("number of hops and number of aliases do not match") t.Fatalf("number of hops=(%v) and number of aliases=(%v) do "+
"not match", len(path), len(nodeAliases))
} }
for i, hop := range path { for i, hop := range path {
@@ -3072,7 +3196,7 @@ func (c *pathFindingTestContext) assertPath(path []*models.CachedEdgePolicy,
// dbFindPath calls findPath after getting a db transaction from the database // dbFindPath calls findPath after getting a db transaction from the database
// graph. // graph.
func dbFindPath(graph *channeldb.ChannelGraph, func dbFindPath(graph *channeldb.ChannelGraph,
additionalEdges map[route.Vertex][]*models.CachedEdgePolicy, additionalEdges map[route.Vertex][]AdditionalEdge,
bandwidthHints bandwidthHints, bandwidthHints bandwidthHints,
r *RestrictParams, cfg *PathFindingConfig, r *RestrictParams, cfg *PathFindingConfig,
source, target route.Vertex, amt lnwire.MilliSatoshi, timePref float64, source, target route.Vertex, amt lnwire.MilliSatoshi, timePref float64,
@@ -3230,8 +3354,8 @@ func TestBlindedRouteConstruction(t *testing.T) {
edges := []*models.CachedEdgePolicy{ edges := []*models.CachedEdgePolicy{
aliceBobEdge, aliceBobEdge,
bobCarolEdge, bobCarolEdge,
carolDaveEdge, carolDaveEdge.EdgePolicy(),
daveEveEdge, daveEveEdge.EdgePolicy(),
} }
// Total timelock for the route should include: // Total timelock for the route should include:
@@ -3327,3 +3451,168 @@ func TestBlindedRouteConstruction(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expectedRoute, route) require.Equal(t, expectedRoute, route)
} }
// TestLastHopPayloadSize tests the final hop payload size. The final hop
// payload structure differes from the intermediate hop payload for both the
// non-blinded and blinded case.
func TestLastHopPayloadSize(t *testing.T) {
t.Parallel()
var (
metadata = []byte{21, 22}
customRecords = map[uint64][]byte{
record.CustomTypeStart: {1, 2, 3},
}
sizeEncryptedData = 100
encrypedData = bytes.Repeat(
[]byte{1}, sizeEncryptedData,
)
_, blindedPoint = btcec.PrivKeyFromBytes([]byte{5})
paymentAddr = &[32]byte{1}
ampOptions = &AMPOptions{}
amtToForward = lnwire.MilliSatoshi(10000)
finalHopExpiry int32 = 144
oneHopBlindedPayment = &BlindedPayment{
BlindedPath: &sphinx.BlindedPath{
BlindedHops: []*sphinx.BlindedHopInfo{
{
CipherText: encrypedData,
},
},
BlindingPoint: blindedPoint,
},
}
twoHopBlindedPayment = &BlindedPayment{
BlindedPath: &sphinx.BlindedPath{
BlindedHops: []*sphinx.BlindedHopInfo{
{
CipherText: encrypedData,
},
{
CipherText: encrypedData,
},
},
BlindingPoint: blindedPoint,
},
}
)
testCases := []struct {
name string
restrictions *RestrictParams
finalHopExpiry int32
amount lnwire.MilliSatoshi
legacy bool
}{
{
name: "Non blinded final hop",
restrictions: &RestrictParams{
PaymentAddr: paymentAddr,
DestCustomRecords: customRecords,
Metadata: metadata,
Amp: ampOptions,
},
amount: amtToForward,
finalHopExpiry: finalHopExpiry,
legacy: false,
},
{
name: "Non blinded final hop legacy",
restrictions: &RestrictParams{
// The legacy encoding has no ability to include
// those extra data we expect that this data is
// ignored.
PaymentAddr: paymentAddr,
DestCustomRecords: customRecords,
Metadata: metadata,
Amp: ampOptions,
},
amount: amtToForward,
finalHopExpiry: finalHopExpiry,
legacy: true,
},
{
name: "Blinded final hop introduction point",
restrictions: &RestrictParams{
BlindedPayment: oneHopBlindedPayment,
},
amount: amtToForward,
finalHopExpiry: finalHopExpiry,
},
{
name: "Blinded final hop of a two hop payment",
restrictions: &RestrictParams{
BlindedPayment: twoHopBlindedPayment,
},
amount: amtToForward,
finalHopExpiry: finalHopExpiry,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var mpp *record.MPP
if tc.restrictions.PaymentAddr != nil {
mpp = record.NewMPP(
tc.amount, *tc.restrictions.PaymentAddr,
)
}
// In case it's an AMP payment we use the max AMP record
// size to estimate the final hop size.
var amp *record.AMP
if tc.restrictions.Amp != nil {
amp = &record.MaxAmpPayLoadSize
}
var finalHop route.Hop
if tc.restrictions.BlindedPayment != nil {
blindedPath := tc.restrictions.BlindedPayment.
BlindedPath.BlindedHops
blindedPoint := tc.restrictions.BlindedPayment.
BlindedPath.BlindingPoint
//nolint:lll
finalHop = route.Hop{
AmtToForward: tc.amount,
OutgoingTimeLock: uint32(tc.finalHopExpiry),
LegacyPayload: false,
EncryptedData: blindedPath[len(blindedPath)-1].CipherText,
}
if len(blindedPath) == 1 {
finalHop.BlindingPoint = blindedPoint
}
} else {
//nolint:lll
finalHop = route.Hop{
LegacyPayload: tc.legacy,
AmtToForward: tc.amount,
OutgoingTimeLock: uint32(tc.finalHopExpiry),
Metadata: tc.restrictions.Metadata,
MPP: mpp,
AMP: amp,
CustomRecords: tc.restrictions.DestCustomRecords,
}
}
payLoad, err := createHopPayload(finalHop, 0, true)
require.NoErrorf(t, err, "failed to create hop payload")
expectedPayloadSize := lastHopPayloadSize(
tc.restrictions, tc.finalHopExpiry,
tc.amount, tc.legacy,
)
require.Equal(
t, expectedPayloadSize,
uint64(payLoad.NumBytes()),
)
})
}
}
+1 -1
View File
@@ -628,7 +628,7 @@ func (p *paymentLifecycle) createNewPaymentAttempt(rt *route.Route,
return nil, err return nil, err
} }
// It this shard carries MPP or AMP options, add them to the last hop // If this shard carries MPP or AMP options, add them to the last hop
// on the route. // on the route.
hop := rt.Hops[len(rt.Hops)-1] hop := rt.Hops[len(rt.Hops)-1]
if shard.MPP() != nil { if shard.MPP() != nil {
+5 -3
View File
@@ -163,7 +163,7 @@ type PaymentSession interface {
// loop if payment attempts take long enough. An additional set of edges can // loop if payment attempts take long enough. An additional set of edges can
// also be provided to assist in reaching the payment's destination. // also be provided to assist in reaching the payment's destination.
type paymentSession struct { type paymentSession struct {
additionalEdges map[route.Vertex][]*models.CachedEdgePolicy additionalEdges map[route.Vertex][]AdditionalEdge
getBandwidthHints func(routingGraph) (bandwidthHints, error) getBandwidthHints func(routingGraph) (bandwidthHints, error)
@@ -258,6 +258,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
DestCustomRecords: p.payment.DestCustomRecords, DestCustomRecords: p.payment.DestCustomRecords,
DestFeatures: p.payment.DestFeatures, DestFeatures: p.payment.DestFeatures,
PaymentAddr: p.payment.PaymentAddr, PaymentAddr: p.payment.PaymentAddr,
Amp: p.payment.amp,
Metadata: p.payment.Metadata, Metadata: p.payment.Metadata,
} }
@@ -441,11 +442,12 @@ func (p *paymentSession) GetAdditionalEdgePolicy(pubKey *btcec.PublicKey,
} }
for _, edge := range edges { for _, edge := range edges {
if edge.ChannelID != channelID { policy := edge.EdgePolicy()
if policy.ChannelID != channelID {
continue continue
} }
return edge return policy
} }
return nil return nil
+7 -3
View File
@@ -95,9 +95,9 @@ func (m *SessionSource) NewPaymentSessionEmpty() PaymentSession {
// RouteHintsToEdges converts a list of invoice route hints to an edge map that // RouteHintsToEdges converts a list of invoice route hints to an edge map that
// can be passed into pathfinding. // can be passed into pathfinding.
func RouteHintsToEdges(routeHints [][]zpay32.HopHint, target route.Vertex) ( func RouteHintsToEdges(routeHints [][]zpay32.HopHint, target route.Vertex) (
map[route.Vertex][]*models.CachedEdgePolicy, error) { map[route.Vertex][]AdditionalEdge, error) {
edges := make(map[route.Vertex][]*models.CachedEdgePolicy) edges := make(map[route.Vertex][]AdditionalEdge)
// Traverse through all of the available hop hints and include them in // Traverse through all of the available hop hints and include them in
// our edges map, indexed by the public key of the channel's starting // our edges map, indexed by the public key of the channel's starting
@@ -127,7 +127,7 @@ func RouteHintsToEdges(routeHints [][]zpay32.HopHint, target route.Vertex) (
// Finally, create the channel edge from the hop hint // Finally, create the channel edge from the hop hint
// and add it to list of edges corresponding to the node // and add it to list of edges corresponding to the node
// at the start of the channel. // at the start of the channel.
edge := &models.CachedEdgePolicy{ edgePolicy := &models.CachedEdgePolicy{
ToNodePubKey: func() route.Vertex { ToNodePubKey: func() route.Vertex {
return endNode.PubKeyBytes return endNode.PubKeyBytes
}, },
@@ -142,6 +142,10 @@ func RouteHintsToEdges(routeHints [][]zpay32.HopHint, target route.Vertex) (
TimeLockDelta: hopHint.CLTVExpiryDelta, TimeLockDelta: hopHint.CLTVExpiryDelta,
} }
edge := &PrivateEdge{
policy: edgePolicy,
}
v := route.NewVertex(hopHint.NodeID) v := route.NewVertex(hopHint.NodeID)
edges[v] = append(edges[v], edge) edges[v] = append(edges[v], edge)
} }
+1 -1
View File
@@ -138,7 +138,7 @@ func TestUpdateAdditionalEdge(t *testing.T) {
require.Equal(t, 1, len(policies), "should have 1 edge policy") require.Equal(t, 1, len(policies), "should have 1 edge policy")
// Check that the policy has been created as expected. // Check that the policy has been created as expected.
policy := policies[0] policy := policies[0].EdgePolicy()
require.Equal(t, testChannelID, policy.ChannelID, "channel ID mismatch") require.Equal(t, testChannelID, policy.ChannelID, "channel ID mismatch")
require.Equal(t, require.Equal(t,
oldExpiryDelta, policy.TimeLockDelta, "timelock delta mismatch", oldExpiryDelta, policy.TimeLockDelta, "timelock delta mismatch",
+1 -1
View File
@@ -1954,7 +1954,7 @@ type RouteRequest struct {
// RouteHints is an alias type for a set of route hints, with the source node // RouteHints is an alias type for a set of route hints, with the source node
// as the map's key and the details of the hint(s) in the edge policy. // as the map's key and the details of the hint(s) in the edge policy.
type RouteHints map[route.Vertex][]*models.CachedEdgePolicy type RouteHints map[route.Vertex][]AdditionalEdge
// NewRouteRequest produces a new route request for a regular payment or one // NewRouteRequest produces a new route request for a regular payment or one
// to a blinded route, validating that the target, routeHints and finalExpiry // to a blinded route, validating that the target, routeHints and finalExpiry
+1 -1
View File
@@ -3954,7 +3954,7 @@ func TestNewRouteRequest(t *testing.T) {
name: "hints and blinded", name: "hints and blinded",
blindedPayment: blindedMultiHop, blindedPayment: blindedMultiHop,
routeHints: make( routeHints: make(
map[route.Vertex][]*models.CachedEdgePolicy, map[route.Vertex][]AdditionalEdge,
), ),
err: ErrHintsAndBlinded, err: ErrHintsAndBlinded,
}, },
+38 -4
View File
@@ -40,9 +40,12 @@ func newNodeEdgeUnifier(sourceNode, toNode route.Vertex,
} }
// addPolicy adds a single channel policy. Capacity may be zero if unknown // addPolicy adds a single channel policy. Capacity may be zero if unknown
// (light clients). // (light clients). We expect a non-nil payload size function and will request a
// graceful shutdown if it is not provided as this indicates that edges are
// incorrectly specified.
func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex, func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex,
edge *models.CachedEdgePolicy, capacity btcutil.Amount) { edge *models.CachedEdgePolicy, capacity btcutil.Amount,
hopPayloadSizeFn PayloadSizeFunc) {
localChan := fromNode == u.sourceNode localChan := fromNode == u.sourceNode
@@ -62,9 +65,20 @@ func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex,
u.edgeUnifiers[fromNode] = unifier u.edgeUnifiers[fromNode] = unifier
} }
// In case no payload size function was provided a graceful shutdown
// is requested, because this function is not used as intended.
if hopPayloadSizeFn == nil {
log.Criticalf("No payloadsize function was provided for the "+
"edge (chanid=%v) when adding it to the edge unifier "+
"of node: %v", edge.ChannelID, fromNode)
return
}
unifier.edges = append(unifier.edges, &unifiedEdge{ unifier.edges = append(unifier.edges, &unifiedEdge{
policy: edge, policy: edge,
capacity: capacity, capacity: capacity,
hopPayloadSizeFn: hopPayloadSizeFn,
}) })
} }
@@ -79,9 +93,13 @@ func (u *nodeEdgeUnifier) addGraphPolicies(g routingGraph) error {
return nil return nil
} }
// Add this policy to the corresponding edgeUnifier. // Add this policy to the corresponding edgeUnifier. We default
// to the clear hop payload size function because
// `addGraphPolicies` is only used for cleartext intermediate
// hops in a route.
u.addPolicy( u.addPolicy(
channel.OtherNode, channel.InPolicy, channel.Capacity, channel.OtherNode, channel.InPolicy, channel.Capacity,
defaultHopPayloadSize,
) )
return nil return nil
@@ -96,6 +114,12 @@ func (u *nodeEdgeUnifier) addGraphPolicies(g routingGraph) error {
type unifiedEdge struct { type unifiedEdge struct {
policy *models.CachedEdgePolicy policy *models.CachedEdgePolicy
capacity btcutil.Amount capacity btcutil.Amount
// hopPayloadSize supplies an edge with the ability to calculate the
// exact payload size if this edge would be included in a route. This
// is needed because hops of a blinded path differ in their payload
// structure compared to cleartext hops.
hopPayloadSizeFn PayloadSizeFunc
} }
// amtInRange checks whether an amount falls within the valid range for a // amtInRange checks whether an amount falls within the valid range for a
@@ -202,6 +226,7 @@ func (u *edgeUnifier) getEdgeLocal(amt lnwire.MilliSatoshi,
log.Debugf("Skipped edge %v: not enough bandwidth, "+ log.Debugf("Skipped edge %v: not enough bandwidth, "+
"bandwidth=%v, amt=%v", edge.policy.ChannelID, "bandwidth=%v, amt=%v", edge.policy.ChannelID,
bandwidth, amt) bandwidth, amt)
continue continue
} }
@@ -214,6 +239,7 @@ func (u *edgeUnifier) getEdgeLocal(amt lnwire.MilliSatoshi,
log.Debugf("Skipped edge %v: not max bandwidth, "+ log.Debugf("Skipped edge %v: not max bandwidth, "+
"bandwidth=%v, maxBandwidth=%v", "bandwidth=%v, maxBandwidth=%v",
bandwidth, maxBandwidth) bandwidth, maxBandwidth)
continue continue
} }
maxBandwidth = bandwidth maxBandwidth = bandwidth
@@ -222,6 +248,7 @@ func (u *edgeUnifier) getEdgeLocal(amt lnwire.MilliSatoshi,
bestEdge = &unifiedEdge{ bestEdge = &unifiedEdge{
policy: edge.policy, policy: edge.policy,
capacity: edge.capacity, capacity: edge.capacity,
hopPayloadSizeFn: edge.hopPayloadSizeFn,
} }
} }
@@ -238,6 +265,7 @@ func (u *edgeUnifier) getEdgeNetwork(amt lnwire.MilliSatoshi) *unifiedEdge {
maxFee lnwire.MilliSatoshi maxFee lnwire.MilliSatoshi
maxTimelock uint16 maxTimelock uint16
maxCapMsat lnwire.MilliSatoshi maxCapMsat lnwire.MilliSatoshi
hopPayloadSizeFn PayloadSizeFunc
) )
for _, edge := range u.edges { for _, edge := range u.edges {
@@ -274,7 +302,6 @@ func (u *edgeUnifier) getEdgeNetwork(amt lnwire.MilliSatoshi) *unifiedEdge {
maxTimelock = lntypes.Max( maxTimelock = lntypes.Max(
maxTimelock, edge.policy.TimeLockDelta, maxTimelock, edge.policy.TimeLockDelta,
) )
// Use the policy that results in the highest fee for this // Use the policy that results in the highest fee for this
// specific amount. // specific amount.
fee := edge.policy.ComputeFee(amt) fee := edge.policy.ComputeFee(amt)
@@ -282,11 +309,17 @@ func (u *edgeUnifier) getEdgeNetwork(amt lnwire.MilliSatoshi) *unifiedEdge {
log.Debugf("Skipped edge %v due to it produces less "+ log.Debugf("Skipped edge %v due to it produces less "+
"fee: fee=%v, maxFee=%v", "fee: fee=%v, maxFee=%v",
edge.policy.ChannelID, fee, maxFee) edge.policy.ChannelID, fee, maxFee)
continue continue
} }
maxFee = fee maxFee = fee
bestPolicy = edge.policy bestPolicy = edge.policy
// The payload size function for edges to a connected peer is
// always the same hence there is not need to find the maximum.
// This also counts for blinded edges where we only have one
// edge to a blinded peer.
hopPayloadSizeFn = edge.hopPayloadSizeFn
} }
// Return early if no channel matches. // Return early if no channel matches.
@@ -308,6 +341,7 @@ func (u *edgeUnifier) getEdgeNetwork(amt lnwire.MilliSatoshi) *unifiedEdge {
modifiedEdge := unifiedEdge{policy: &policyCopy} modifiedEdge := unifiedEdge{policy: &policyCopy}
modifiedEdge.policy.TimeLockDelta = maxTimelock modifiedEdge.policy.TimeLockDelta = maxTimelock
modifiedEdge.capacity = maxCapMsat.ToSatoshis() modifiedEdge.capacity = maxCapMsat.ToSatoshis()
modifiedEdge.hopPayloadSizeFn = hopPayloadSizeFn
return &modifiedEdge return &modifiedEdge
} }
+7 -5
View File
@@ -41,15 +41,17 @@ func TestNodeEdgeUnifier(t *testing.T) {
c2 := btcutil.Amount(8) c2 := btcutil.Amount(8)
unifierFilled := newNodeEdgeUnifier(source, toNode, nil) unifierFilled := newNodeEdgeUnifier(source, toNode, nil)
unifierFilled.addPolicy(fromNode, &p1, c1) unifierFilled.addPolicy(fromNode, &p1, c1, defaultHopPayloadSize)
unifierFilled.addPolicy(fromNode, &p2, c2) unifierFilled.addPolicy(fromNode, &p2, c2, defaultHopPayloadSize)
unifierNoCapacity := newNodeEdgeUnifier(source, toNode, nil) unifierNoCapacity := newNodeEdgeUnifier(source, toNode, nil)
unifierNoCapacity.addPolicy(fromNode, &p1, 0) unifierNoCapacity.addPolicy(fromNode, &p1, 0, defaultHopPayloadSize)
unifierNoCapacity.addPolicy(fromNode, &p2, 0) unifierNoCapacity.addPolicy(fromNode, &p2, 0, defaultHopPayloadSize)
unifierNoInfo := newNodeEdgeUnifier(source, toNode, nil) unifierNoInfo := newNodeEdgeUnifier(source, toNode, nil)
unifierNoInfo.addPolicy(fromNode, &models.CachedEdgePolicy{}, 0) unifierNoInfo.addPolicy(
fromNode, &models.CachedEdgePolicy{}, 0, defaultHopPayloadSize,
)
tests := []struct { tests := []struct {
name string name string