routing: add inbound fee support to pathfinding

Add sender-side support for inbound fees in pathfinding
and route building.
This commit is contained in:
Joost Jager committed 2024-03-31 18:12:28 +02:00
1 parent d97e7d30fb
commit 0bae781785
13 files changed
+585 -131

No files matched your search

+5 -2
View File
@@ -103,7 +103,10 @@ func (g *CachedGraph) FetchAmountPairCapacity(nodeFrom, nodeTo route.Vertex,
amount lnwire.MilliSatoshi) (btcutil.Amount, error) {
// Create unified edges for all incoming connections.
u := newNodeEdgeUnifier(g.sourceNode(), nodeTo, nil)
//
// Note: Inbound fees are not used here because this method is only used
// by a deprecated router rpc.
u := newNodeEdgeUnifier(g.sourceNode(), nodeTo, false, nil)
err := u.addGraphPolicies(g)
if err != nil {
@@ -116,7 +119,7 @@ func (g *CachedGraph) FetchAmountPairCapacity(nodeFrom, nodeTo route.Vertex,
nodeFrom, nodeTo)
}
edge := edgeUnifier.getEdgeNetwork(amount)
edge := edgeUnifier.getEdgeNetwork(amount, 0)
if edge == nil {
return 0, fmt.Errorf("no edge for node pair %v -> %v "+
"(amount %v)", nodeFrom, nodeTo, amount)
+9 -3
View File
@@ -18,10 +18,16 @@ type nodeWithDist struct {
// outgoing edges (channels) emanating from a node.
node route.Vertex
// amountToReceive is the amount that should be received by this node.
// netAmountReceived is the amount that should be received by this node.
// Either as final payment to the final node or as an intermediate
// amount that includes also the fees for subsequent hops.
amountToReceive lnwire.MilliSatoshi
// amount that includes also the fees for subsequent hops. This node's
// inbound fee is already subtracted from the htlc amount - if
// applicable.
netAmountReceived lnwire.MilliSatoshi
// outboundFee is the fee that this node charges on the outgoing
// channel.
outboundFee lnwire.MilliSatoshi
// incomingCltv is the expected absolute expiry height for the incoming
// htlc of this node. This value should already include the final cltv
+101 -39
View File
@@ -11,6 +11,7 @@ import (
"github.com/btcsuite/btcd/btcutil"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/feature"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
@@ -118,7 +119,7 @@ type finalHopParams struct {
// makes calculating the totals during route construction difficult if we
// include blinded paths on the first pass).
//
// NOTE: The passed slice of ChannelHops MUST be sorted in forward order: from
// NOTE: The passed slice of unified edges MUST be sorted in forward order: from
// the source to the target node of the path finding attempt. It is assumed that
// any feature vectors on all hops have been validated for transitive
// dependencies.
@@ -157,7 +158,7 @@ func newRoute(sourceVertex route.Vertex,
// we compute the route in reverse.
var (
amtToForward lnwire.MilliSatoshi
fee lnwire.MilliSatoshi
fee int64
totalAmtMsatBlinded lnwire.MilliSatoshi
outgoingTimeLock uint32
tlvPayload bool
@@ -195,7 +196,7 @@ func newRoute(sourceVertex route.Vertex,
// Fee is not part of the hop payload, but only used for
// reporting through RPC. Set to zero for the final hop.
fee = lnwire.MilliSatoshi(0)
fee = 0
// As this is the last hop, we'll use the specified
// final CLTV delta value instead of the value from the
@@ -244,7 +245,18 @@ func newRoute(sourceVertex route.Vertex,
// and its policy for the outgoing channel. This policy
// is stored as part of the incoming channel of
// the next hop.
fee = pathEdges[i+1].policy.ComputeFee(amtToForward)
outboundFee := pathEdges[i+1].policy.ComputeFee(
amtToForward,
)
inboundFee := pathEdges[i].inboundFees.CalcFee(
amtToForward + outboundFee,
)
fee = int64(outboundFee) + inboundFee
if fee < 0 {
fee = 0
}
// We'll take the total timelock of the preceding hop as
// the outgoing timelock or this hop. Then we'll
@@ -275,7 +287,7 @@ func newRoute(sourceVertex route.Vertex,
// Finally, we update the amount that needs to flow into the
// *next* hop, which is the amount this hop needs to forward,
// accounting for the fee that it takes.
nextIncomingAmount = amtToForward + fee
nextIncomingAmount = amtToForward + lnwire.MilliSatoshi(fee)
}
// If we are creating a route to a blinded path, we need to add some
@@ -660,13 +672,13 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// Don't record the initial partial path in the distance map and reserve
// that key for the source key in the case we route to ourselves.
partialPath := &nodeWithDist{
dist: 0,
weight: 0,
node: target,
amountToReceive: amt,
incomingCltv: finalHtlcExpiry,
probability: 1,
routingInfoSize: lastHopPayloadSize,
dist: 0,
weight: 0,
node: target,
netAmountReceived: amt,
incomingCltv: finalHtlcExpiry,
probability: 1,
routingInfoSize: lastHopPayloadSize,
}
// Calculate the absolute cltv limit. Use uint64 to prevent an overflow
@@ -703,9 +715,27 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
edgesExpanded++
// Calculate inbound fee charged by "to" node. The exit hop
// doesn't charge inbound fees. If the "to" node is the exit
// hop, its inbound fees have already been set to zero by
// nodeEdgeUnifier.
inboundFee := edge.inboundFees.CalcFee(
toNodeDist.netAmountReceived,
)
// Make sure that the node total fee is never negative.
// Routing nodes treat a total fee that turns out
// negative as a zero fee and pathfinding should do the
// same.
minInboundFee := -int64(toNodeDist.outboundFee)
if inboundFee < minInboundFee {
inboundFee = minInboundFee
}
// Calculate amount that the candidate node would have to send
// out.
amountToSend := toNodeDist.amountToReceive
amountToSend := toNodeDist.netAmountReceived +
lnwire.MilliSatoshi(inboundFee)
// Request the success probability for this edge.
edgeProbability := r.ProbabilitySource(
@@ -735,10 +765,15 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// Also determine the time lock delta that will be added to the
// route if fromVertex is selected. If fromVertex is the source
// node, no additional timelock is required.
var fee lnwire.MilliSatoshi
var timeLockDelta uint16
var (
timeLockDelta uint16
outboundFee int64
)
if fromVertex != source {
fee = edge.policy.ComputeFee(amountToSend)
outboundFee = int64(
edge.policy.ComputeFee(amountToSend),
)
timeLockDelta = edge.policy.TimeLockDelta
}
@@ -749,17 +784,19 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
return
}
// amountToReceive is the amount that the node that is added to
// the distance map needs to receive from a (to be found)
// previous node in the route. That previous node will need to
// pay the amount that this node forwards plus the fee it
// charges.
amountToReceive := amountToSend + fee
// netAmountToReceive is the amount that the node that is added
// to the distance map needs to receive from a (to be found)
// previous node in the route. The inbound fee of the receiving
// node is already subtracted from this value. The previous node
// will need to pay the amount that this node forwards plus the
// fee it charges plus this node's inbound fee.
netAmountToReceive := amountToSend +
lnwire.MilliSatoshi(outboundFee)
// Check if accumulated fees would exceed fee limit when this
// node would be added to the path.
totalFee := amountToReceive - amt
if totalFee > r.FeeLimit {
totalFee := int64(netAmountToReceive) - int64(amt)
if totalFee > 0 && lnwire.MilliSatoshi(totalFee) > r.FeeLimit {
return
}
@@ -775,11 +812,21 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
return
}
// Calculate the combined fee for this edge. Dijkstra does not
// support negative edge weights. Because this fee feeds into
// the edge weight calculation, we don't allow it to be
// negative.
signedFee := inboundFee + outboundFee
fee := lnwire.MilliSatoshi(0)
if signedFee > 0 {
fee = lnwire.MilliSatoshi(signedFee)
}
// By adding fromVertex in the route, there will be an extra
// weight composed of the fee that this node will charge and
// the amount that will be locked for timeLockDelta blocks in
// the HTLC that is handed out to fromVertex.
weight := edgeWeight(amountToReceive, fee, timeLockDelta)
weight := edgeWeight(netAmountToReceive, fee, timeLockDelta)
// Compute the tentative weight to this new channel/edge
// which is the weight from our toNode to the target node
@@ -787,7 +834,10 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
tempWeight := toNodeDist.weight + weight
// Add an extra factor to the weight to take into account the
// probability.
// probability. Another reason why we rounded the fee up to zero
// is to prevent a highly negative fee from cancelling out the
// extra factor. We don't want an always-failing node to attract
// traffic using a highly negative fee and escape penalization.
tempDist := getProbabilityBasedDist(
tempWeight, probability,
absoluteAttemptCost,
@@ -854,14 +904,15 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// The new better distance is recorded, and also our "next hop"
// map is populated with this edge.
withDist := &nodeWithDist{
dist: tempDist,
weight: tempWeight,
node: fromVertex,
amountToReceive: amountToReceive,
incomingCltv: incomingCltv,
probability: probability,
nextHop: edge,
routingInfoSize: routingInfoSize,
dist: tempDist,
weight: tempWeight,
node: fromVertex,
netAmountReceived: netAmountToReceive,
outboundFee: lnwire.MilliSatoshi(outboundFee),
incomingCltv: incomingCltv,
probability: probability,
nextHop: edge,
routingInfoSize: routingInfoSize,
}
distance[fromVertex] = withDist
@@ -920,9 +971,13 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
nodesVisited++
pivot := partialPath.node
isExitHop := partialPath.nextHop == nil
// Create unified edges for all incoming connections.
u := newNodeEdgeUnifier(self, pivot, outgoingChanMap)
// Create unified policies for all incoming connections. Don't
// use inbound fees for the exit hop.
u := newNodeEdgeUnifier(
self, pivot, !isExitHop, outgoingChanMap,
)
err := u.addGraphPolicies(g.graph)
if err != nil {
@@ -931,6 +986,11 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// We add hop hints that were supplied externally.
for _, reverseEdge := range additionalEdgesWithSrc[pivot] {
// Assume zero inbound fees for route hints. If inbound
// fees would apply, they couldn't be communicated in
// bolt11 invoices currently.
inboundFee := models.InboundFee{}
// Hop hints don't contain a capacity. We set one here,
// since a capacity is needed for probability
// calculations. We set a high capacity to act as if
@@ -942,12 +1002,13 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
u.addPolicy(
reverseEdge.sourceNode,
reverseEdge.edge.EdgePolicy(),
inboundFee,
fakeHopHintCapacity,
reverseEdge.edge.IntermediatePayloadSize,
)
}
amtToSend := partialPath.amountToReceive
netAmountReceived := partialPath.netAmountReceived
// Expand all connections using the optimal policy for each
// connection.
@@ -969,7 +1030,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
}
edge := edgeUnifier.getEdge(
amtToSend, g.bandwidthHints,
netAmountReceived, g.bandwidthHints,
partialPath.outboundFee,
)
if edge == nil {
@@ -1050,7 +1112,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
log.Debugf("Found route: probability=%v, hops=%v, fee=%v",
distance[source].probability, len(pathEdges),
distance[source].amountToReceive-amt)
distance[source].netAmountReceived-amt)
return pathEdges, distance[source].probability, nil
}
+169 -8
View File
@@ -417,14 +417,16 @@ func parseTestGraph(t *testing.T, useCache bool, path string) (
}
type testChannelPolicy struct {
Expiry uint16
MinHTLC lnwire.MilliSatoshi
MaxHTLC lnwire.MilliSatoshi
FeeBaseMsat lnwire.MilliSatoshi
FeeRate lnwire.MilliSatoshi
LastUpdate time.Time
Disabled bool
Features *lnwire.FeatureVector
Expiry uint16
MinHTLC lnwire.MilliSatoshi
MaxHTLC lnwire.MilliSatoshi
FeeBaseMsat lnwire.MilliSatoshi
FeeRate lnwire.MilliSatoshi
InboundFeeBaseMsat int64
InboundFeeRate int64
LastUpdate time.Time
Disabled bool
Features *lnwire.FeatureVector
}
type testChannelEnd struct {
@@ -662,6 +664,19 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
return nil, err
}
getExtraData := func(
end *testChannelEnd) lnwire.ExtraOpaqueData {
var extraData lnwire.ExtraOpaqueData
inboundFee := lnwire.Fee{
BaseFee: int32(end.InboundFeeBaseMsat),
FeeRate: int32(end.InboundFeeRate),
}
require.NoError(t, extraData.PackRecords(&inboundFee))
return extraData
}
if node1.testChannelPolicy != nil {
var msgFlags lnwire.ChanUpdateMsgFlags
if node1.MaxHTLC != 0 {
@@ -684,6 +699,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
FeeBaseMSat: node1.FeeBaseMsat,
FeeProportionalMillionths: node1.FeeRate,
ToNode: node2Vertex,
ExtraOpaqueData: getExtraData(node1),
}
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
return nil, err
@@ -713,6 +729,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
FeeBaseMSat: node2.FeeBaseMsat,
FeeProportionalMillionths: node2.FeeRate,
ToNode: node1Vertex,
ExtraOpaqueData: getExtraData(node2),
}
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
return nil, err
@@ -809,6 +826,9 @@ func TestPathFinding(t *testing.T) {
}, {
name: "with metadata",
fn: runFindPathWithMetadata,
}, {
name: "inbound fees",
fn: runInboundFees,
}}
// Run with graph cache enabled.
@@ -3129,6 +3149,147 @@ func runRouteToSelf(t *testing.T, useCache bool) {
ctx.assertPath(path, []uint64{1, 3, 2})
}
// runInboundFees tests whether correct routes are built when inbound fees
// apply.
func runInboundFees(t *testing.T, useCache bool) {
// Setup a test network.
chanCapSat := btcutil.Amount(100000)
features := lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(
lnwire.PaymentAddrOptional,
lnwire.TLVOnionPayloadRequired,
),
lnwire.Features,
)
testChannels := []*testChannel{
asymmetricTestChannel("a", "b", chanCapSat,
&testChannelPolicy{
MinHTLC: lnwire.NewMSatFromSatoshis(2),
Features: features,
},
&testChannelPolicy{
Features: features,
InboundFeeRate: -60000,
InboundFeeBaseMsat: -5000,
}, 10,
),
asymmetricTestChannel("b", "c", chanCapSat,
&testChannelPolicy{
Expiry: 20,
FeeRate: 50000,
FeeBaseMsat: 1000,
MinHTLC: lnwire.NewMSatFromSatoshis(2),
Features: features,
},
&testChannelPolicy{
Features: features,
InboundFeeRate: 0,
InboundFeeBaseMsat: 5000,
}, 11,
),
asymmetricTestChannel("c", "d", chanCapSat,
&testChannelPolicy{
Expiry: 20,
FeeRate: 50000,
FeeBaseMsat: 0,
MinHTLC: lnwire.NewMSatFromSatoshis(2),
Features: features,
},
&testChannelPolicy{
Features: features,
InboundFeeRate: -50000,
InboundFeeBaseMsat: -8000,
}, 12,
),
asymmetricTestChannel("d", "e", chanCapSat,
&testChannelPolicy{
Expiry: 20,
FeeRate: 100000,
FeeBaseMsat: 9000,
MinHTLC: lnwire.NewMSatFromSatoshis(2),
Features: features,
},
&testChannelPolicy{
Features: features,
InboundFeeRate: 80000,
InboundFeeBaseMsat: 2000,
}, 13,
),
}
ctx := newPathFindingTestContext(t, useCache, testChannels, "a")
payAddr := [32]byte{1}
ctx.restrictParams.PaymentAddr = &payAddr
ctx.restrictParams.DestFeatures = tlvPayAddrFeatures
const (
startingHeight = 100
finalHopCLTV = 1
)
paymentAmt := lnwire.MilliSatoshi(100_000)
target := ctx.keyFromAlias("e")
path, err := ctx.findPath(target, paymentAmt)
require.NoError(t, err, "unable to find path")
rt, err := newRoute(
ctx.source, path, startingHeight,
finalHopParams{
amt: paymentAmt,
cltvDelta: finalHopCLTV,
records: nil,
paymentAddr: &payAddr,
totalAmt: paymentAmt,
},
nil,
)
require.NoError(t, err, "unable to create path")
expectedHops := []*route.Hop{
{
PubKeyBytes: ctx.keyFromAlias("b"),
ChannelID: 10,
// The amount that c forwards (105_050) plus the out fee
// (5_252) and in fee (5_000) of c.
AmtToForward: 115_302,
OutgoingTimeLock: 141,
},
{
PubKeyBytes: ctx.keyFromAlias("c"),
ChannelID: 11,
// The amount that d forwards (100_000) plus the out fee
// (19_000) and in fee (-13_950) of d.
AmtToForward: 105_050,
OutgoingTimeLock: 121,
},
{
PubKeyBytes: ctx.keyFromAlias("d"),
ChannelID: 12,
AmtToForward: 100_000,
OutgoingTimeLock: 101,
},
{
PubKeyBytes: ctx.keyFromAlias("e"),
ChannelID: 13,
AmtToForward: 100_000,
OutgoingTimeLock: 101,
MPP: record.NewMPP(100_000, payAddr),
},
}
expectedRt := &route.Route{
// The amount that b forwards (115_302) plus the out fee (6_765)
// and in fee (-12324) of b. The total fee is floored at zero.
TotalAmount: 115_302,
TotalTimeLock: 161,
SourcePubKey: ctx.keyFromAlias("a"),
Hops: expectedHops,
}
require.Equal(t, expectedRt, rt)
}
type pathFindingTestContext struct {
t *testing.T
graph *channeldb.ChannelGraph
+9 -5
View File
@@ -3161,9 +3161,13 @@ func getRouteUnifiers(source route.Vertex, hops []route.Vertex,
localChan := i == 0
// Build unified edges for this hop based on the channels known
// in the graph.
u := newNodeEdgeUnifier(source, toNode, outgoingChans)
// Build unified policies for this hop based on the channels
// known in the graph. Don't use inbound fees.
//
// TODO: Add inbound fees support for BuildRoute.
u := newNodeEdgeUnifier(
source, toNode, false, outgoingChans,
)
err := u.addGraphPolicies(graph)
if err != nil {
@@ -3189,7 +3193,7 @@ 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)
edge := edgeUnifier.getEdge(runningAmt, bandwidthHints, 0)
if edge == nil {
log.Errorf("Cannot find policy with amt=%v for node %v",
runningAmt, fromNode)
@@ -3227,7 +3231,7 @@ 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)
edge := unifier.getEdge(receiverAmt, bandwidthHints, 0)
if edge == nil {
fromNode := source
if i > 0 {
+95 -25
View File
@@ -1,6 +1,8 @@
package routing
import (
"math"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
@@ -21,6 +23,9 @@ type nodeEdgeUnifier struct {
// toNode is the node for which the edge unifiers are instantiated.
toNode route.Vertex
// useInboundFees indicates whether to take inbound fees into account.
useInboundFees bool
// outChanRestr is an optional outgoing channel restriction for the
// local channel to use.
outChanRestr map[uint64]struct{}
@@ -28,14 +33,15 @@ type nodeEdgeUnifier struct {
// newNodeEdgeUnifier instantiates a new nodeEdgeUnifier object. Channel
// policies can be added to this object.
func newNodeEdgeUnifier(sourceNode, toNode route.Vertex,
func newNodeEdgeUnifier(sourceNode, toNode route.Vertex, useInboundFees bool,
outChanRestr map[uint64]struct{}) *nodeEdgeUnifier {
return &nodeEdgeUnifier{
edgeUnifiers: make(map[route.Vertex]*edgeUnifier),
toNode: toNode,
sourceNode: sourceNode,
outChanRestr: outChanRestr,
edgeUnifiers: make(map[route.Vertex]*edgeUnifier),
toNode: toNode,
useInboundFees: useInboundFees,
sourceNode: sourceNode,
outChanRestr: outChanRestr,
}
}
@@ -44,8 +50,8 @@ func newNodeEdgeUnifier(sourceNode, toNode route.Vertex,
// graceful shutdown if it is not provided as this indicates that edges are
// incorrectly specified.
func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex,
edge *models.CachedEdgePolicy, capacity btcutil.Amount,
hopPayloadSizeFn PayloadSizeFunc) {
edge *models.CachedEdgePolicy, inboundFee models.InboundFee,
capacity btcutil.Amount, hopPayloadSizeFn PayloadSizeFunc) {
localChan := fromNode == u.sourceNode
@@ -75,10 +81,16 @@ func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex,
return
}
// Zero inbound fee for exit hops.
if !u.useInboundFees {
inboundFee = models.InboundFee{}
}
unifier.edges = append(unifier.edges, &unifiedEdge{
policy: edge,
capacity: capacity,
hopPayloadSizeFn: hopPayloadSizeFn,
inboundFees: inboundFee,
})
}
@@ -97,9 +109,13 @@ func (u *nodeEdgeUnifier) addGraphPolicies(g routingGraph) error {
// to the clear hop payload size function because
// `addGraphPolicies` is only used for cleartext intermediate
// hops in a route.
inboundFee := models.NewInboundFeeFromWire(
channel.InboundFee,
)
u.addPolicy(
channel.OtherNode, channel.InPolicy, channel.Capacity,
defaultHopPayloadSize,
channel.OtherNode, channel.InPolicy, inboundFee,
channel.Capacity, defaultHopPayloadSize,
)
return nil
@@ -112,8 +128,9 @@ func (u *nodeEdgeUnifier) addGraphPolicies(g routingGraph) error {
// unifiedEdge is the individual channel data that is kept inside an edgeUnifier
// object.
type unifiedEdge struct {
policy *models.CachedEdgePolicy
capacity btcutil.Amount
policy *models.CachedEdgePolicy
capacity btcutil.Amount
inboundFees models.InboundFee
// hopPayloadSize supplies an edge with the ability to calculate the
// exact payload size if this edge would be included in a route. This
@@ -163,20 +180,41 @@ type edgeUnifier struct {
// getEdge returns the optimal unified edge to use for this connection given a
// specific amount to send. It differentiates between local and network
// channels.
func (u *edgeUnifier) getEdge(amt lnwire.MilliSatoshi,
bandwidthHints bandwidthHints) *unifiedEdge {
func (u *edgeUnifier) getEdge(netAmtReceived lnwire.MilliSatoshi,
bandwidthHints bandwidthHints,
nextOutFee lnwire.MilliSatoshi) *unifiedEdge {
if u.localChan {
return u.getEdgeLocal(amt, bandwidthHints)
return u.getEdgeLocal(
netAmtReceived, bandwidthHints, nextOutFee,
)
}
return u.getEdgeNetwork(amt)
return u.getEdgeNetwork(netAmtReceived, nextOutFee)
}
// calcCappedInboundFee calculates the inbound fee for a channel, taking into
// account the total node fee for the "to" node.
func calcCappedInboundFee(edge *unifiedEdge, amt lnwire.MilliSatoshi,
nextOutFee lnwire.MilliSatoshi) int64 {
// Calculate the inbound fee charged for the amount that passes over the
// channel.
inboundFee := edge.inboundFees.CalcFee(amt)
// Take into account that the total node fee cannot be negative.
if inboundFee < -int64(nextOutFee) {
inboundFee = -int64(nextOutFee)
}
return inboundFee
}
// getEdgeLocal returns the optimal unified edge to use for this local
// connection given a specific amount to send.
func (u *edgeUnifier) getEdgeLocal(amt lnwire.MilliSatoshi,
bandwidthHints bandwidthHints) *unifiedEdge {
func (u *edgeUnifier) getEdgeLocal(netAmtReceived lnwire.MilliSatoshi,
bandwidthHints bandwidthHints,
nextOutFee lnwire.MilliSatoshi) *unifiedEdge {
var (
bestEdge *unifiedEdge
@@ -184,10 +222,20 @@ func (u *edgeUnifier) getEdgeLocal(amt lnwire.MilliSatoshi,
)
for _, edge := range u.edges {
// Calculate the inbound fee charged at the receiving node.
inboundFee := calcCappedInboundFee(
edge, netAmtReceived, nextOutFee,
)
// Add inbound fee to get to the amount that is sent over the
// local channel.
amt := netAmtReceived + lnwire.MilliSatoshi(inboundFee)
// Check valid amount range for the channel.
if !edge.amtInRange(amt) {
log.Debugf("Amount %v not in range for edge %v",
amt, edge.policy.ChannelID)
netAmtReceived, edge.policy.ChannelID)
continue
}
@@ -249,6 +297,7 @@ func (u *edgeUnifier) getEdgeLocal(amt lnwire.MilliSatoshi,
policy: edge.policy,
capacity: edge.capacity,
hopPayloadSizeFn: edge.hopPayloadSizeFn,
inboundFees: edge.inboundFees,
}
}
@@ -259,16 +308,27 @@ func (u *edgeUnifier) getEdgeLocal(amt lnwire.MilliSatoshi,
// given a specific amount to send. The goal is to return a unified edge with a
// policy that maximizes the probability of a successful forward in a non-strict
// forwarding context.
func (u *edgeUnifier) getEdgeNetwork(amt lnwire.MilliSatoshi) *unifiedEdge {
func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi,
nextOutFee lnwire.MilliSatoshi) *unifiedEdge {
var (
bestPolicy *models.CachedEdgePolicy
maxFee lnwire.MilliSatoshi
bestPolicy *unifiedEdge
maxFee int64 = math.MinInt64
maxTimelock uint16
maxCapMsat lnwire.MilliSatoshi
hopPayloadSizeFn PayloadSizeFunc
)
for _, edge := range u.edges {
// Calculate the inbound fee charged at the receiving node.
inboundFee := calcCappedInboundFee(
edge, netAmtReceived, nextOutFee,
)
// Add inbound fee to get to the amount that is sent over the
// channel.
amt := netAmtReceived + lnwire.MilliSatoshi(inboundFee)
// Check valid amount range for the channel.
if !edge.amtInRange(amt) {
log.Debugf("Amount %v not in range for edge %v",
@@ -302,9 +362,12 @@ func (u *edgeUnifier) getEdgeNetwork(amt lnwire.MilliSatoshi) *unifiedEdge {
maxTimelock = lntypes.Max(
maxTimelock, edge.policy.TimeLockDelta,
)
outboundFee := int64(edge.policy.ComputeFee(amt))
fee := outboundFee + inboundFee
// Use the policy that results in the highest fee for this
// specific amount.
fee := edge.policy.ComputeFee(amt)
if fee < maxFee {
log.Debugf("Skipped edge %v due to it produces less "+
"fee: fee=%v, maxFee=%v",
@@ -314,7 +377,11 @@ func (u *edgeUnifier) getEdgeNetwork(amt lnwire.MilliSatoshi) *unifiedEdge {
}
maxFee = fee
bestPolicy = edge.policy
bestPolicy = &unifiedEdge{
policy: edge.policy,
inboundFees: edge.inboundFees,
}
// 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
@@ -337,8 +404,11 @@ func (u *edgeUnifier) getEdgeNetwork(amt lnwire.MilliSatoshi) *unifiedEdge {
// get forwarded. Because we penalize pair-wise, there won't be a second
// chance for this node pair. But this is all only needed for nodes that
// have distinct policies for channels to the same peer.
policyCopy := *bestPolicy
modifiedEdge := unifiedEdge{policy: &policyCopy}
policyCopy := *bestPolicy.policy
modifiedEdge := unifiedEdge{
policy: &policyCopy,
inboundFees: bestPolicy.inboundFees,
}
modifiedEdge.policy.TimeLockDelta = maxTimelock
modifiedEdge.capacity = maxCapMsat.ToSatoshis()
modifiedEdge.hopPayloadSizeFn = hopPayloadSizeFn
+109 -27
View File
@@ -46,31 +46,75 @@ func TestNodeEdgeUnifier(t *testing.T) {
c1 := btcutil.Amount(7)
c2 := btcutil.Amount(8)
unifierFilled := newNodeEdgeUnifier(source, toNode, nil)
unifierFilled.addPolicy(fromNode, &p1, c1, defaultHopPayloadSize)
unifierFilled.addPolicy(fromNode, &p2, c2, defaultHopPayloadSize)
inboundFee1 := models.InboundFee{
Base: 5,
Rate: 10000,
}
unifierNoCapacity := newNodeEdgeUnifier(source, toNode, nil)
unifierNoCapacity.addPolicy(fromNode, &p1, 0, defaultHopPayloadSize)
unifierNoCapacity.addPolicy(fromNode, &p2, 0, defaultHopPayloadSize)
inboundFee2 := models.InboundFee{
Base: 10,
Rate: 10000,
}
unifierNoInfo := newNodeEdgeUnifier(source, toNode, nil)
unifierNoInfo.addPolicy(
fromNode, &models.CachedEdgePolicy{}, 0, defaultHopPayloadSize,
unifierFilled := newNodeEdgeUnifier(source, toNode, false, nil)
unifierFilled.addPolicy(
fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize,
)
unifierFilled.addPolicy(
fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize,
)
unifierLocal := newNodeEdgeUnifier(fromNode, toNode, nil)
unifierLocal.addPolicy(fromNode, &p1, c1, defaultHopPayloadSize)
unifierNoCapacity := newNodeEdgeUnifier(source, toNode, false, nil)
unifierNoCapacity.addPolicy(
fromNode, &p1, inboundFee1, 0, defaultHopPayloadSize,
)
unifierNoCapacity.addPolicy(
fromNode, &p2, inboundFee2, 0, defaultHopPayloadSize,
)
unifierNoInfo := newNodeEdgeUnifier(source, toNode, false, nil)
unifierNoInfo.addPolicy(
fromNode, &models.CachedEdgePolicy{}, models.InboundFee{},
0, defaultHopPayloadSize,
)
unifierInboundFee := newNodeEdgeUnifier(source, toNode, true, nil)
unifierInboundFee.addPolicy(
fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize,
)
unifierInboundFee.addPolicy(
fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize,
)
unifierLocal := newNodeEdgeUnifier(fromNode, toNode, true, nil)
unifierLocal.addPolicy(
fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize,
)
inboundFeeZero := models.InboundFee{}
inboundFeeNegative := models.InboundFee{
Base: -150,
}
unifierNegInboundFee := newNodeEdgeUnifier(source, toNode, true, nil)
unifierNegInboundFee.addPolicy(
fromNode, &p1, inboundFeeZero, c1, defaultHopPayloadSize,
)
unifierNegInboundFee.addPolicy(
fromNode, &p2, inboundFeeNegative, c2, defaultHopPayloadSize,
)
tests := []struct {
name string
unifier *nodeEdgeUnifier
amount lnwire.MilliSatoshi
expectedFeeBase lnwire.MilliSatoshi
expectedFeeRate lnwire.MilliSatoshi
expectedTimeLock uint16
expectNoPolicy bool
expectedCapacity btcutil.Amount
name string
unifier *nodeEdgeUnifier
amount lnwire.MilliSatoshi
expectedFeeBase lnwire.MilliSatoshi
expectedFeeRate lnwire.MilliSatoshi
expectedInboundFee models.InboundFee
expectedTimeLock uint16
expectNoPolicy bool
expectedCapacity btcutil.Amount
nextOutFee lnwire.MilliSatoshi
}{
{
name: "amount below min htlc",
@@ -132,13 +176,49 @@ func TestNodeEdgeUnifier(t *testing.T) {
expectNoPolicy: true,
},
{
name: "local",
unifier: unifierLocal,
amount: 100,
expectedFeeBase: p1.FeeBaseMSat,
expectedFeeRate: p1.FeeProportionalMillionths,
expectedTimeLock: p1.TimeLockDelta,
expectedCapacity: c1,
name: "local",
unifier: unifierLocal,
amount: 100,
expectedFeeBase: p1.FeeBaseMSat,
expectedFeeRate: p1.FeeProportionalMillionths,
expectedTimeLock: p1.TimeLockDelta,
expectedCapacity: c1,
expectedInboundFee: inboundFee1,
},
{
name: "use p2 with highest fee " +
"including inbound",
unifier: unifierInboundFee,
amount: 200,
expectedFeeBase: p2.FeeBaseMSat,
expectedFeeRate: p2.FeeProportionalMillionths,
expectedInboundFee: inboundFee2,
expectedTimeLock: p1.TimeLockDelta,
expectedCapacity: c2,
},
// Choose inbound fee exactly so that max htlc is just exceeded.
// In this test, the amount that must be sent is 5001 msat.
{
name: "inbound fee exceeds max htlc",
unifier: unifierInboundFee,
amount: 4947,
expectNoPolicy: true,
},
// The outbound fee of p2 is higher than p1, but because of the
// inbound fee on p2 it is brought down to 0. Purely based on
// total channel fee, p1 would be selected as the highest fee
// channel. However, because the total node fee can never be
// negative and the next outgoing fee is zero, the effect of the
// inbound discount is cancelled out.
{
name: "inbound fee that is rounded up",
unifier: unifierNegInboundFee,
amount: 500,
expectedFeeBase: p2.FeeBaseMSat,
expectedFeeRate: p2.FeeProportionalMillionths,
expectedInboundFee: inboundFeeNegative,
expectedTimeLock: p1.TimeLockDelta,
expectedCapacity: c2,
},
}
@@ -149,7 +229,7 @@ func TestNodeEdgeUnifier(t *testing.T) {
t.Parallel()
edge := test.unifier.edgeUnifiers[fromNode].getEdge(
test.amount, bandwidthHints,
test.amount, bandwidthHints, test.nextOutFee,
)
if test.expectNoPolicy {
@@ -163,6 +243,8 @@ func TestNodeEdgeUnifier(t *testing.T) {
policy.FeeBaseMSat, "base fee")
require.Equal(t, test.expectedFeeRate,
policy.FeeProportionalMillionths, "fee rate")
require.Equal(t, test.expectedInboundFee,
edge.inboundFees, "inbound fee")
require.Equal(t, test.expectedTimeLock,
policy.TimeLockDelta, "timelock")
require.Equal(t, test.expectedCapacity, edge.capacity,