mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-20 13:34:32 +01: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:
parent
d97e7d30fb
commit
0bae781785
13 changed files with 585 additions and 131 deletions
|
@ -490,6 +490,16 @@ func (c *ChannelGraph) ForEachNodeDirectedChannel(tx kvdb.RTx,
|
|||
cachedInPolicy.ToNodeFeatures = toNodeFeatures
|
||||
}
|
||||
|
||||
var inboundFee lnwire.Fee
|
||||
if p1 != nil {
|
||||
// Extract inbound fee. If there is a decoding error,
|
||||
// skip this edge.
|
||||
_, err := p1.ExtraOpaqueData.ExtractRecords(&inboundFee)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
directedChannel := &DirectedChannel{
|
||||
ChannelID: e.ChannelID,
|
||||
IsNode1: node == e.NodeKey1Bytes,
|
||||
|
@ -497,6 +507,7 @@ func (c *ChannelGraph) ForEachNodeDirectedChannel(tx kvdb.RTx,
|
|||
Capacity: e.Capacity,
|
||||
OutPolicySet: p1 != nil,
|
||||
InPolicy: cachedInPolicy,
|
||||
InboundFee: inboundFee,
|
||||
}
|
||||
|
||||
if node == e.NodeKey2Bytes {
|
||||
|
|
|
@ -59,6 +59,9 @@ type DirectedChannel struct {
|
|||
// source, so we're always interested in the edge that arrives to us
|
||||
// from the other node.
|
||||
InPolicy *models.CachedEdgePolicy
|
||||
|
||||
// Inbound fees of this node.
|
||||
InboundFee lnwire.Fee
|
||||
}
|
||||
|
||||
// DeepCopy creates a deep copy of the channel, including the incoming policy.
|
||||
|
@ -220,6 +223,14 @@ func (c *GraphCache) updateOrAddEdge(node route.Vertex, edge *DirectedChannel) {
|
|||
func (c *GraphCache) UpdatePolicy(policy *models.ChannelEdgePolicy, fromNode,
|
||||
toNode route.Vertex, edge1 bool) {
|
||||
|
||||
// Extract inbound fee if possible and available. If there is a decoding
|
||||
// error, ignore this policy.
|
||||
var inboundFee lnwire.Fee
|
||||
_, err := policy.ExtraOpaqueData.ExtractRecords(&inboundFee)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.mtx.Lock()
|
||||
defer c.mtx.Unlock()
|
||||
|
||||
|
@ -240,11 +251,13 @@ func (c *GraphCache) UpdatePolicy(policy *models.ChannelEdgePolicy, fromNode,
|
|||
// policy for node 1.
|
||||
case channel.IsNode1 && edge1:
|
||||
channel.OutPolicySet = true
|
||||
channel.InboundFee = inboundFee
|
||||
|
||||
// This is node 2, and it is edge 2, so this is the outgoing
|
||||
// policy for node 2.
|
||||
case !channel.IsNode1 && !edge1:
|
||||
channel.OutPolicySet = true
|
||||
channel.InboundFee = inboundFee
|
||||
|
||||
// The other two cases left mean it's the inbound policy for the
|
||||
// node.
|
||||
|
|
|
@ -75,6 +75,10 @@ func TestGraphCacheAddNode(t *testing.T) {
|
|||
ChannelID: 1000,
|
||||
ChannelFlags: lnwire.ChanUpdateChanFlags(channelFlagA),
|
||||
ToNode: nodeB,
|
||||
// Define an inbound fee.
|
||||
ExtraOpaqueData: []byte{
|
||||
253, 217, 3, 8, 0, 0, 0, 10, 0, 0, 0, 20,
|
||||
},
|
||||
}
|
||||
inPolicy1 := &models.ChannelEdgePolicy{
|
||||
ChannelID: 1000,
|
||||
|
@ -124,8 +128,18 @@ func TestGraphCacheAddNode(t *testing.T) {
|
|||
edges map[uint64]*DirectedChannel) error {
|
||||
|
||||
nodes[node] = struct{}{}
|
||||
for chanID := range edges {
|
||||
for chanID, directedChannel := range edges {
|
||||
chans[chanID] = struct{}{}
|
||||
|
||||
if node == nodeA {
|
||||
require.NotZero(
|
||||
t, directedChannel.InboundFee,
|
||||
)
|
||||
} else {
|
||||
require.Zero(
|
||||
t, directedChannel.InboundFee,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -674,7 +674,7 @@ func createChannelEdge(db kvdb.Backend, node1, node2 *LightningNode) (
|
|||
FeeBaseMSat: 4352345,
|
||||
FeeProportionalMillionths: 3452352,
|
||||
ToNode: secondNode,
|
||||
ExtraOpaqueData: []byte("new unknown feature2"),
|
||||
ExtraOpaqueData: []byte{1, 0},
|
||||
}
|
||||
edge2 := &models.ChannelEdgePolicy{
|
||||
SigBytes: testSig.Serialize(),
|
||||
|
@ -688,7 +688,7 @@ func createChannelEdge(db kvdb.Backend, node1, node2 *LightningNode) (
|
|||
FeeBaseMSat: 4352345,
|
||||
FeeProportionalMillionths: 90392423,
|
||||
ToNode: firstNode,
|
||||
ExtraOpaqueData: []byte("new unknown feature1"),
|
||||
ExtraOpaqueData: []byte{1, 0},
|
||||
}
|
||||
|
||||
return edgeInfo, edge1, edge2
|
||||
|
@ -3929,7 +3929,17 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) {
|
|||
require.Nil(t, err)
|
||||
|
||||
// Create an edge and add it to the db.
|
||||
edgeInfo, _, _ := createChannelEdge(graph.db, node1, node2)
|
||||
edgeInfo, e1, e2 := createChannelEdge(graph.db, node1, node2)
|
||||
|
||||
// Because of lexigraphical sorting and the usage of random node keys in
|
||||
// this test, we need to determine which edge belongs to node 1 at
|
||||
// runtime.
|
||||
var edge1 *models.ChannelEdgePolicy
|
||||
if e1.ToNode == node2.PubKeyBytes {
|
||||
edge1 = e1
|
||||
} else {
|
||||
edge1 = e2
|
||||
}
|
||||
|
||||
// Add the channel, but only insert a single edge into the graph.
|
||||
require.NoError(t, graph.AddChannelEdge(edgeInfo))
|
||||
|
@ -3952,6 +3962,28 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) {
|
|||
// We should be able to accumulate the single channel added, even
|
||||
// though we have a nil edge policy here.
|
||||
require.NotNil(t, getSingleChannel())
|
||||
|
||||
// Set an inbound fee and check that it is properly returned.
|
||||
edge1.ExtraOpaqueData = []byte{
|
||||
253, 217, 3, 8, 0, 0, 0, 10, 0, 0, 0, 20,
|
||||
}
|
||||
require.NoError(t, graph.UpdateEdgePolicy(edge1))
|
||||
|
||||
directedChan := getSingleChannel()
|
||||
require.NotNil(t, directedChan)
|
||||
require.Equal(t, directedChan.InboundFee, lnwire.Fee{
|
||||
BaseFee: 10,
|
||||
FeeRate: 20,
|
||||
})
|
||||
|
||||
// Set an invalid inbound fee and check that the edge is no longer
|
||||
// returned.
|
||||
edge1.ExtraOpaqueData = []byte{
|
||||
253, 217, 3, 8, 0,
|
||||
}
|
||||
require.NoError(t, graph.UpdateEdgePolicy(edge1))
|
||||
|
||||
require.Nil(t, getSingleChannel())
|
||||
}
|
||||
|
||||
// TestGraphLoading asserts that the cache is properly reconstructed after a
|
||||
|
|
|
@ -117,9 +117,8 @@
|
|||
node operators to require senders to pay an inbound fee for forwards and
|
||||
payments. It is recommended to only use negative fees (an inbound "discount")
|
||||
initially to keep the channels open for senders that do not recognize inbound
|
||||
fees. In this release, no send support for pathfinding and route building is
|
||||
added yet. We first want to learn more about the impact that inbound fees have
|
||||
on the routing economy.
|
||||
fees. [Send support](https://github.com/lightningnetwork/lnd/pull/6934) is
|
||||
implemented as well.
|
||||
|
||||
* A new config value,
|
||||
[sweeper.maxfeerate](https://github.com/lightningnetwork/lnd/pull/7823), is
|
||||
|
|
|
@ -70,7 +70,7 @@ func testMultiHopPayments(ht *lntest.HarnessTest) {
|
|||
// channel edges to relatively large non default values. This makes it
|
||||
// possible to pick up more subtle fee calculation errors.
|
||||
maxHtlc := lntest.CalculateMaxHtlc(chanAmt)
|
||||
const aliceBaseFeeSat = 1
|
||||
const aliceBaseFeeSat = 20
|
||||
const aliceFeeRatePPM = 100000
|
||||
updateChannelPolicy(
|
||||
ht, alice, chanPointAlice, aliceBaseFeeSat*1000,
|
||||
|
@ -81,8 +81,8 @@ func testMultiHopPayments(ht *lntest.HarnessTest) {
|
|||
// Define a negative inbound fee for Alice, to verify that this is
|
||||
// backwards compatible with an older sender ignoring the discount.
|
||||
const (
|
||||
aliceInboundBaseFeeMsat = -1
|
||||
aliceInboundFeeRate = -10000
|
||||
aliceInboundBaseFeeMsat = -2000
|
||||
aliceInboundFeeRate = -50000 // 5%
|
||||
)
|
||||
|
||||
updateChannelPolicy(
|
||||
|
@ -119,12 +119,11 @@ func testMultiHopPayments(ht *lntest.HarnessTest) {
|
|||
ht.AssertAmountPaid("Alice(local) => Bob(remote)", alice,
|
||||
chanPointAlice, expectedAmountPaidAtoB, int64(0))
|
||||
|
||||
// To forward a payment of 1000 sat, Alice is charging a fee of 1 sat +
|
||||
// 10% = 101 sat. Note that this does not include the inbound fee
|
||||
// (discount) because there is no sender support yet.
|
||||
const aliceFeePerPayment = aliceBaseFeeSat +
|
||||
(paymentAmt * aliceFeeRatePPM / 1_000_000)
|
||||
const expectedFeeAlice = numPayments * aliceFeePerPayment
|
||||
// To forward a payment of 1000 sat, Alice is charging a fee of 20 sat +
|
||||
// 10% = 120 sat, plus the inbound fee over 1120 (= 1000 + 120) sat of
|
||||
// -2 sat - 5% = -58 sat. This makes a total of 62 sat per payment. For
|
||||
// 5 payments, it works out to 310 sat.
|
||||
const expectedFeeAlice = 310
|
||||
|
||||
// Dave needs to pay what Alice pays plus Alice's fee.
|
||||
expectedAmountPaidDtoA := expectedAmountPaidAtoB + expectedFeeAlice
|
||||
|
@ -134,12 +133,10 @@ func testMultiHopPayments(ht *lntest.HarnessTest) {
|
|||
ht.AssertAmountPaid("Dave(local) => Alice(remote)", dave,
|
||||
chanPointDave, expectedAmountPaidDtoA, int64(0))
|
||||
|
||||
// To forward a payment of 1101 sat, Dave is charging a fee of
|
||||
// 5 sat + 15% = 170.15 sat. This is rounded down in rpcserver to 170.
|
||||
const davePaymentAmt = paymentAmt + aliceFeePerPayment
|
||||
const daveFeePerPayment = daveBaseFeeSat +
|
||||
(davePaymentAmt * daveFeeRatePPM / 1_000_000)
|
||||
const expectedFeeDave = numPayments * daveFeePerPayment
|
||||
// To forward a payment of 1062 sat, Dave is charging a fee of 5 sat +
|
||||
// 15% = 164.3 sat. For 5 payments this is 821.5 sat. This test works
|
||||
// with sats, so we need to round down to 821.
|
||||
const expectedFeeDave = 821
|
||||
|
||||
// Carol needs to pay what Dave pays plus Dave's fee.
|
||||
expectedAmountPaidCtoD := expectedAmountPaidDtoA + expectedFeeDave
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue