mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-07-12 14:12:27 +02:00
routing: add inbound fee support to pathfinding
Add sender-side support for inbound fees in pathfinding and route building.
This commit is contained in:
@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user